From 2c786364bc37ac1c9fce9a131193edfce75f2611 Mon Sep 17 00:00:00 2001 From: DongHun Kwak Date: Thu, 6 Apr 2023 09:10:41 +0900 Subject: [PATCH] Import mp4parse 0.12.0 --- .cargo_vcs_info.json | 5 + Cargo.toml | 66 + Cargo.toml.orig | 55 + LICENSE | 373 ++ README.md | 2 + benches/avif_benchmark.rs | 23 + src/boxes.rs | 234 + src/lib.rs | 5254 ++++++++++++++++++++ src/macros.rs | 12 + src/tests.rs | 1323 +++++ src/unstable.rs | 546 ++ tests/1x1-black-alpha-50pct-premultiplied.avif | Bin 0 -> 1106 bytes tests/amr_nb_1f.3gp | Bin 0 -> 701 bytes tests/amr_wb_1f.3gp | Bin 0 -> 713 bytes tests/av1C-missing-essential.avif | Bin 0 -> 334 bytes tests/bad-ipma-flags.avif | Bin 0 -> 437 bytes tests/bad-ipma-version.avif | Bin 0 -> 435 bytes tests/bbb_sunflower_QCIF_30fps_h263_noaudio_1f.3gp | Bin 0 -> 1578 bytes tests/clap-basic-1_3x3-to-1x1.avif | Bin 0 -> 355 bytes ...terfuzz-testcase-minimized-mp4-6093954524250112 | Bin 0 -> 56 bytes tests/corrupt/bug-1655846.avif | Bin 0 -> 256 bytes tests/corrupt/bug-1661347.avif | Bin 0 -> 8468 bytes tests/corrupt/hdlr-not-first.avif | Bin 0 -> 334 bytes tests/corrupt/hdlr-not-pict.avif | Bin 0 -> 334 bytes tests/corrupt/imir-before-clap.avif | Bin 0 -> 345 bytes tests/corrupt/invalid-avif-colr-multiple-nclx.avif | Bin 0 -> 315 bytes tests/corrupt/invalid-avif-colr-multiple-prof.avif | Bin 0 -> 864 bytes tests/corrupt/invalid-avif-colr-multiple-rICC.avif | Bin 0 -> 864 bytes tests/corrupt/invalid-avif-colr-multiple.zip | Bin 0 -> 1963 bytes tests/corrupt/ipma-duplicate-item_id.avif | Bin 0 -> 49032 bytes .../corrupt/ipma-duplicate-version-and-flags.avif | Bin 0 -> 49032 bytes tests/corrupt/ipma-invalid-property-index.avif | Bin 0 -> 294 bytes tests/corrupt/no-alpha-av1C.avif | Bin 0 -> 437 bytes tests/corrupt/no-av1C.avif | Bin 0 -> 281 bytes tests/corrupt/no-hdlr.avif | Bin 0 -> 294 bytes tests/corrupt/no-ispe.avif | Bin 0 -> 273 bytes tests/corrupt/no-pixi-for-alpha.avif | Bin 0 -> 522 bytes tests/corrupt/no-pixi.avif | Bin 0 -> 317 bytes tests/hdlr-nonzero-reserved.avif | Bin 0 -> 294 bytes tests/imir-missing-essential.avif | Bin 0 -> 84996 bytes tests/irot-missing-essential.avif | Bin 0 -> 84837 bytes tests/multiple-extents.avif | Bin 0 -> 342 bytes tests/no-mif1.avif | Bin 0 -> 334 bytes tests/overflow.rs | 15 + tests/public.rs | 1215 +++++ tests/valid-alpha.avif | Bin 0 -> 450 bytes tests/valid-avif-colr-nclx-and-prof-and-rICC.avif | Bin 0 -> 1452 bytes tests/valid-avif-colr-nclx-and-prof.avif | Bin 0 -> 883 bytes tests/valid-avif-colr-nclx-and-rICC.avif | Bin 0 -> 883 bytes tests/valid-avif-colr-nclx.avif | Bin 0 -> 314 bytes tests/valid-avif-colr-prof-and-rICC.avif | Bin 0 -> 1432 bytes tests/valid-avif-colr-prof.avif | Bin 0 -> 863 bytes tests/valid-avif-colr-rICC.avif | Bin 0 -> 863 bytes tests/valid.avif | Bin 0 -> 294 bytes 54 files changed, 9123 insertions(+) create mode 100644 .cargo_vcs_info.json create mode 100644 Cargo.toml create mode 100644 Cargo.toml.orig create mode 100644 LICENSE create mode 100644 README.md create mode 100644 benches/avif_benchmark.rs create mode 100644 src/boxes.rs create mode 100644 src/lib.rs create mode 100644 src/macros.rs create mode 100644 src/tests.rs create mode 100644 src/unstable.rs create mode 100644 tests/1x1-black-alpha-50pct-premultiplied.avif create mode 100644 tests/amr_nb_1f.3gp create mode 100644 tests/amr_wb_1f.3gp create mode 100644 tests/av1C-missing-essential.avif create mode 100644 tests/bad-ipma-flags.avif create mode 100644 tests/bad-ipma-version.avif create mode 100644 tests/bbb_sunflower_QCIF_30fps_h263_noaudio_1f.3gp create mode 100644 tests/clap-basic-1_3x3-to-1x1.avif create mode 100644 tests/clusterfuzz-testcase-minimized-mp4-6093954524250112 create mode 100644 tests/corrupt/bug-1655846.avif create mode 100644 tests/corrupt/bug-1661347.avif create mode 100644 tests/corrupt/hdlr-not-first.avif create mode 100644 tests/corrupt/hdlr-not-pict.avif create mode 100644 tests/corrupt/imir-before-clap.avif create mode 100644 tests/corrupt/invalid-avif-colr-multiple-nclx.avif create mode 100644 tests/corrupt/invalid-avif-colr-multiple-prof.avif create mode 100644 tests/corrupt/invalid-avif-colr-multiple-rICC.avif create mode 100644 tests/corrupt/invalid-avif-colr-multiple.zip create mode 100644 tests/corrupt/ipma-duplicate-item_id.avif create mode 100644 tests/corrupt/ipma-duplicate-version-and-flags.avif create mode 100644 tests/corrupt/ipma-invalid-property-index.avif create mode 100644 tests/corrupt/no-alpha-av1C.avif create mode 100644 tests/corrupt/no-av1C.avif create mode 100644 tests/corrupt/no-hdlr.avif create mode 100644 tests/corrupt/no-ispe.avif create mode 100644 tests/corrupt/no-pixi-for-alpha.avif create mode 100644 tests/corrupt/no-pixi.avif create mode 100644 tests/hdlr-nonzero-reserved.avif create mode 100644 tests/imir-missing-essential.avif create mode 100644 tests/irot-missing-essential.avif create mode 100644 tests/multiple-extents.avif create mode 100644 tests/no-mif1.avif create mode 100644 tests/overflow.rs create mode 100644 tests/public.rs create mode 100644 tests/valid-alpha.avif create mode 100644 tests/valid-avif-colr-nclx-and-prof-and-rICC.avif create mode 100644 tests/valid-avif-colr-nclx-and-prof.avif create mode 100644 tests/valid-avif-colr-nclx-and-rICC.avif create mode 100644 tests/valid-avif-colr-nclx.avif create mode 100644 tests/valid-avif-colr-prof-and-rICC.avif create mode 100644 tests/valid-avif-colr-prof.avif create mode 100644 tests/valid-avif-colr-rICC.avif create mode 100644 tests/valid.avif diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json new file mode 100644 index 0000000..0d7f400 --- /dev/null +++ b/.cargo_vcs_info.json @@ -0,0 +1,5 @@ +{ + "git": { + "sha1": "e133798fb00969cad0561f0e92291d2d02803497" + } +} diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..e369c2b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,66 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +name = "mp4parse" +version = "0.12.0" +authors = ["Ralph Giles ", "Matthew Gregan ", "Alfredo Yang ", "Jon Bauman ", "Bryce Seager van Dyk "] +exclude = ["*.mp4", "av1-avif/*"] +description = "Parser for ISO base media file format (mp4)" +documentation = "https://docs.rs/mp4parse/" +categories = ["multimedia::video"] +license = "MPL-2.0" +repository = "https://github.com/mozilla/mp4parse-rust" + +[lib] +bench = false + +[[bench]] +name = "avif_benchmark" +harness = false +[dependencies.bitreader] +version = "0.3.2" + +[dependencies.byteorder] +version = "1.2.1" + +[dependencies.env_logger] +version = "0.8" + +[dependencies.fallible_collections] +version = "0.4" +features = ["std_io"] + +[dependencies.log] +version = "0.4" + +[dependencies.num-traits] +version = "0.2.14" + +[dependencies.static_assertions] +version = "1.1.0" +[dev-dependencies.criterion] +version = "0.3" + +[dev-dependencies.test-assembler] +version = "0.1.2" + +[dev-dependencies.walkdir] +version = "2.3.1" + +[features] +3gpp = [] +meta-xml = [] +missing-pixi-permitted = [] +mp4v = [] +unstable-api = [] +[badges.travis-ci] +repository = "https://github.com/mozilla/mp4parse-rust" diff --git a/Cargo.toml.orig b/Cargo.toml.orig new file mode 100644 index 0000000..1639edd --- /dev/null +++ b/Cargo.toml.orig @@ -0,0 +1,55 @@ +[package] +name = "mp4parse" +version = "0.12.0" +authors = [ + "Ralph Giles ", + "Matthew Gregan ", + "Alfredo Yang ", + "Jon Bauman ", + "Bryce Seager van Dyk ", +] + +description = "Parser for ISO base media file format (mp4)" +documentation = "https://docs.rs/mp4parse/" +license = "MPL-2.0" +categories = ["multimedia::video"] + +repository = "https://github.com/mozilla/mp4parse-rust" + +# Avoid complaints about trying to package test files. +exclude = [ + "*.mp4", + "av1-avif/*" +] + +[badges] +travis-ci = { repository = "https://github.com/mozilla/mp4parse-rust" } + +[dependencies] +byteorder = "1.2.1" +bitreader = { version = "0.3.2" } +env_logger = "0.8" +fallible_collections = { version = "0.4", features = ["std_io"] } +num-traits = "0.2.14" +log = "0.4" +static_assertions = "1.1.0" + +[dev-dependencies] +test-assembler = "0.1.2" +walkdir = "2.3.1" +criterion = "0.3" + +[features] +missing-pixi-permitted = [] +3gpp = [] +meta-xml = [] +unstable-api = [] +mp4v = [] + +[[bench]] +name = "avif_benchmark" +harness = false + +# See https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench-gives-unrecognized-option-errors-for-valid-command-line-options +[lib] +bench = false diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..14e2f77 --- /dev/null +++ b/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c9e65a4 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +`mp4parse` is a parser for ISO base media file format (mp4) written in rust. +See [the README in the mp4parse-rust repo](https://github.com/mozilla/mp4parse-rust/blob/master/README.md) for more details. \ No newline at end of file diff --git a/benches/avif_benchmark.rs b/benches/avif_benchmark.rs new file mode 100644 index 0000000..0d8a48b --- /dev/null +++ b/benches/avif_benchmark.rs @@ -0,0 +1,23 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +extern crate criterion; +extern crate mp4parse as mp4; + +use criterion::{criterion_group, criterion_main, Criterion}; +use std::fs::File; + +fn criterion_benchmark(c: &mut Criterion) { + c.bench_function("avif_largest", |b| b.iter(avif_largest)); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); + +fn avif_largest() { + let input = &mut File::open( + "av1-avif/testFiles/Netflix/avif/cosmos_frame05000_yuv444_12bpc_bt2020_pq_qlossless.avif", + ) + .expect("Unknown file"); + assert!(mp4::read_avif(input, mp4::ParseStrictness::Normal).is_ok()); +} diff --git a/src/boxes.rs b/src/boxes.rs new file mode 100644 index 0000000..d80798c --- /dev/null +++ b/src/boxes.rs @@ -0,0 +1,234 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +use std::fmt; + +// To ensure we don't use stdlib allocating types by accident +#[allow(dead_code)] +struct Vec; +#[allow(dead_code)] +struct Box; +#[allow(dead_code)] +struct HashMap; +#[allow(dead_code)] +struct String; + +macro_rules! box_database { + ($($(#[$attr:meta])* $boxenum:ident $boxtype:expr),*,) => { + #[derive(Clone, Copy, PartialEq)] + pub enum BoxType { + $($(#[$attr])* $boxenum),*, + UnknownBox(u32), + } + + impl From for BoxType { + fn from(t: u32) -> BoxType { + use self::BoxType::*; + match t { + $($(#[$attr])* $boxtype => $boxenum),*, + _ => UnknownBox(t), + } + } + } + + impl From for u32 { + fn from(b: BoxType) -> u32 { + use self::BoxType::*; + match b { + $($(#[$attr])* $boxenum => $boxtype),*, + UnknownBox(t) => t, + } + } + } + + } +} + +impl fmt::Debug for BoxType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let fourcc: FourCC = From::from(*self); + fourcc.fmt(f) + } +} + +#[derive(Default, Eq, Hash, PartialEq, Clone)] +pub struct FourCC { + pub value: [u8; 4], +} + +impl From for FourCC { + fn from(number: u32) -> FourCC { + FourCC { + value: number.to_be_bytes(), + } + } +} + +impl From for FourCC { + fn from(t: BoxType) -> FourCC { + let box_num: u32 = Into::into(t); + From::from(box_num) + } +} + +impl From<[u8; 4]> for FourCC { + fn from(v: [u8; 4]) -> FourCC { + FourCC { value: v } + } +} + +impl fmt::Debug for FourCC { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match std::str::from_utf8(&self.value) { + Ok(s) => f.write_str(s), + Err(_) => self.value.fmt(f), + } + } +} + +impl fmt::Display for FourCC { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(std::str::from_utf8(&self.value).unwrap_or("null")) + } +} + +impl PartialEq<&[u8; 4]> for FourCC { + fn eq(&self, other: &&[u8; 4]) -> bool { + self.value.eq(*other) + } +} + +box_database!( + FileTypeBox 0x6674_7970, // "ftyp" + MediaDataBox 0x6d64_6174, // "mdat" + PrimaryItemBox 0x7069_746d, // "pitm" + ItemInfoBox 0x6969_6e66, // "iinf" + ItemInfoEntry 0x696e_6665, // "infe" + ItemLocationBox 0x696c_6f63, // "iloc" + MovieBox 0x6d6f_6f76, // "moov" + MovieHeaderBox 0x6d76_6864, // "mvhd" + TrackBox 0x7472_616b, // "trak" + TrackHeaderBox 0x746b_6864, // "tkhd" + EditBox 0x6564_7473, // "edts" + MediaBox 0x6d64_6961, // "mdia" + EditListBox 0x656c_7374, // "elst" + MediaHeaderBox 0x6d64_6864, // "mdhd" + HandlerBox 0x6864_6c72, // "hdlr" + MediaInformationBox 0x6d69_6e66, // "minf" + ItemReferenceBox 0x6972_6566, // "iref" + ItemPropertiesBox 0x6970_7270, // "iprp" + ItemPropertyContainerBox 0x6970_636f, // "ipco" + ItemPropertyAssociationBox 0x6970_6d61, // "ipma" + ColourInformationBox 0x636f_6c72, // "colr" + ImageSpatialExtentsProperty 0x6973_7065, // "ispe" + PixelInformationBox 0x7069_7869, // "pixi" + AuxiliaryTypeProperty 0x6175_7843, // "auxC" + CleanApertureBox 0x636c_6170, // "clap" + ImageRotation 0x6972_6f74, // "irot" + ImageMirror 0x696d_6972, // "imir" + OperatingPointSelectorProperty 0x6131_6f70, // "a1op" + AV1LayeredImageIndexingProperty 0x6131_6c78, // "a1lx" + LayerSelectorProperty 0x6c73_656c, // "lsel" + SampleTableBox 0x7374_626c, // "stbl" + SampleDescriptionBox 0x7374_7364, // "stsd" + TimeToSampleBox 0x7374_7473, // "stts" + SampleToChunkBox 0x7374_7363, // "stsc" + SampleSizeBox 0x7374_737a, // "stsz" + ChunkOffsetBox 0x7374_636f, // "stco" + ChunkLargeOffsetBox 0x636f_3634, // "co64" + SyncSampleBox 0x7374_7373, // "stss" + AVCSampleEntry 0x6176_6331, // "avc1" + AVC3SampleEntry 0x6176_6333, // "avc3" - Need to check official name in spec. + AVCConfigurationBox 0x6176_6343, // "avcC" + H263SampleEntry 0x7332_3633, // "s263" + H263SpecificBox 0x6432_3633, // "d263" + MP4AudioSampleEntry 0x6d70_3461, // "mp4a" + MP4VideoSampleEntry 0x6d70_3476, // "mp4v" + #[cfg(feature = "3gpp")] + AMRNBSampleEntry 0x7361_6d72, // "samr" - AMR narrow-band + #[cfg(feature = "3gpp")] + AMRWBSampleEntry 0x7361_7762, // "sawb" - AMR wide-band + #[cfg(feature = "3gpp")] + AMRSpecificBox 0x6461_6d72, // "damr" + ESDBox 0x6573_6473, // "esds" + VP8SampleEntry 0x7670_3038, // "vp08" + VP9SampleEntry 0x7670_3039, // "vp09" + VPCodecConfigurationBox 0x7670_6343, // "vpcC" + AV1SampleEntry 0x6176_3031, // "av01" + AV1CodecConfigurationBox 0x6176_3143, // "av1C" + FLACSampleEntry 0x664c_6143, // "fLaC" + FLACSpecificBox 0x6466_4c61, // "dfLa" + OpusSampleEntry 0x4f70_7573, // "Opus" + OpusSpecificBox 0x644f_7073, // "dOps" + ProtectedVisualSampleEntry 0x656e_6376, // "encv" - Need to check official name in spec. + ProtectedAudioSampleEntry 0x656e_6361, // "enca" - Need to check official name in spec. + MovieExtendsBox 0x6d76_6578, // "mvex" + MovieExtendsHeaderBox 0x6d65_6864, // "mehd" + QTWaveAtom 0x7761_7665, // "wave" - quicktime atom + ProtectionSystemSpecificHeaderBox 0x7073_7368, // "pssh" + SchemeInformationBox 0x7363_6869, // "schi" + TrackEncryptionBox 0x7465_6e63, // "tenc" + ProtectionSchemeInfoBox 0x7369_6e66, // "sinf" + OriginalFormatBox 0x6672_6d61, // "frma" + SchemeTypeBox 0x7363_686d, // "schm" + MP3AudioSampleEntry 0x2e6d_7033, // ".mp3" - from F4V. + CompositionOffsetBox 0x6374_7473, // "ctts" + LPCMAudioSampleEntry 0x6c70_636d, // "lpcm" - quicktime atom + ALACSpecificBox 0x616c_6163, // "alac" - Also used by ALACSampleEntry + UuidBox 0x7575_6964, // "uuid" + MetadataBox 0x6d65_7461, // "meta" + MetadataHeaderBox 0x6d68_6472, // "mhdr" + MetadataItemKeysBox 0x6b65_7973, // "keys" + MetadataItemListEntry 0x696c_7374, // "ilst" + MetadataItemDataEntry 0x6461_7461, // "data" + MetadataItemNameBox 0x6e61_6d65, // "name" + #[cfg(feature = "meta-xml")] + MetadataXMLBox 0x786d_6c20, // "xml " + #[cfg(feature = "meta-xml")] + MetadataBXMLBox 0x6278_6d6c, // "bxml" + UserdataBox 0x7564_7461, // "udta" + AlbumEntry 0xa961_6c62, // "©alb" + ArtistEntry 0xa941_5254, // "©ART" + ArtistLowercaseEntry 0xa961_7274, // "©art" + AlbumArtistEntry 0x6141_5254, // "aART" + CommentEntry 0xa963_6d74, // "©cmt" + DateEntry 0xa964_6179, // "©day" + TitleEntry 0xa96e_616d, // "©nam" + CustomGenreEntry 0xa967_656e, // "©gen" + StandardGenreEntry 0x676e_7265, // "gnre" + TrackNumberEntry 0x7472_6b6e, // "trkn" + DiskNumberEntry 0x6469_736b, // "disk" + ComposerEntry 0xa977_7274, // "©wrt" + EncoderEntry 0xa974_6f6f, // "©too" + EncodedByEntry 0xa965_6e63, // "©enc" + TempoEntry 0x746d_706f, // "tmpo" + CopyrightEntry 0x6370_7274, // "cprt" + CompilationEntry 0x6370_696c, // "cpil" + CoverArtEntry 0x636f_7672, // "covr" + AdvisoryEntry 0x7274_6e67, // "rtng" + RatingEntry 0x7261_7465, // "rate" + GroupingEntry 0xa967_7270, // "©grp" + MediaTypeEntry 0x7374_696b, // "stik" + PodcastEntry 0x7063_7374, // "pcst" + CategoryEntry 0x6361_7467, // "catg" + KeywordEntry 0x6b65_7977, // "keyw" + PodcastUrlEntry 0x7075_726c, // "purl" + PodcastGuidEntry 0x6567_6964, // "egid" + DescriptionEntry 0x6465_7363, // "desc" + LongDescriptionEntry 0x6c64_6573, // "ldes" + LyricsEntry 0xa96c_7972, // "©lyr" + TVNetworkNameEntry 0x7476_6e6e, // "tvnn" + TVShowNameEntry 0x7476_7368, // "tvsh" + TVEpisodeNameEntry 0x7476_656e, // "tven" + TVSeasonNumberEntry 0x7476_736e, // "tvsn" + TVEpisodeNumberEntry 0x7476_6573, // "tves" + PurchaseDateEntry 0x7075_7264, // "purd" + GaplessPlaybackEntry 0x7067_6170, // "pgap" + OwnerEntry 0x6f77_6e72, // "ownr" + HDVideoEntry 0x6864_7664, // "hdvd" + SortNameEntry 0x736f_6e6d, // "sonm" + SortAlbumEntry 0x736f_616c, // "soal" + SortArtistEntry 0x736f_6172, // "soar" + SortAlbumArtistEntry 0x736f_6161, // "soaa" + SortComposerEntry 0x736f_636f, // "soco" +); diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..464944f --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,5254 @@ +//! Module for parsing ISO Base Media Format aka video/mp4 streams. + +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +// `clippy::upper_case_acronyms` is a nightly-only lint as of 2021-03-15, so we +// allow `clippy::unknown_clippy_lints` to ignore it on stable - but +// `clippy::unknown_clippy_lints` has been renamed in nightly, so we need to +// allow `renamed_and_removed_lints` to ignore a warning for that. +#![allow(renamed_and_removed_lints)] +#![allow(clippy::unknown_clippy_lints)] +#![allow(clippy::upper_case_acronyms)] + +#[macro_use] +extern crate log; + +extern crate bitreader; +extern crate byteorder; +extern crate fallible_collections; +extern crate num_traits; +use bitreader::{BitReader, ReadInto}; +use byteorder::{ReadBytesExt, WriteBytesExt}; + +use fallible_collections::TryRead; +use fallible_collections::TryReserveError; + +use num_traits::Num; +use std::convert::{TryFrom, TryInto as _}; +use std::fmt; +use std::io::Cursor; +use std::io::{Read, Take}; + +#[macro_use] +mod macros; + +mod boxes; +use boxes::{BoxType, FourCC}; + +// Unit tests. +#[cfg(test)] +mod tests; + +#[cfg(feature = "unstable-api")] +pub mod unstable; + +/// The 'mif1' brand indicates structural requirements on files +/// See HEIF (ISO 23008-12:2017) § 10.2.1 +const MIF1_BRAND: FourCC = FourCC { value: *b"mif1" }; + +/// A trait to indicate a type can be infallibly converted to `u64`. +/// This should only be implemented for infallible conversions, so only unsigned types are valid. +trait ToU64 { + // Remove when https://github.com/rust-lang/rust-clippy/issues/6727 is resolved + #[allow(clippy::wrong_self_convention)] + fn to_u64(self) -> u64; +} + +/// Statically verify that the platform `usize` can fit within a `u64`. +/// If the size won't fit on the given platform, this will fail at compile time, but if a type +/// which can fail TryInto is used, it may panic. +impl ToU64 for usize { + fn to_u64(self) -> u64 { + static_assertions::const_assert!( + std::mem::size_of::() <= std::mem::size_of::() + ); + self.try_into().expect("usize -> u64 conversion failed") + } +} + +/// A trait to indicate a type can be infallibly converted to `usize`. +/// This should only be implemented for infallible conversions, so only unsigned types are valid. +pub trait ToUsize { + fn to_usize(self) -> usize; +} + +/// Statically verify that the given type can fit within a `usize`. +/// If the size won't fit on the given platform, this will fail at compile time, but if a type +/// which can fail TryInto is used, it may panic. +macro_rules! impl_to_usize_from { + ( $from_type:ty ) => { + impl ToUsize for $from_type { + fn to_usize(self) -> usize { + static_assertions::const_assert!( + std::mem::size_of::<$from_type>() <= std::mem::size_of::() + ); + self.try_into().expect(concat!( + stringify!($from_type), + " -> usize conversion failed" + )) + } + } + }; +} + +impl_to_usize_from!(u8); +impl_to_usize_from!(u16); +impl_to_usize_from!(u32); + +/// Indicate the current offset (i.e., bytes already read) in a reader +trait Offset { + fn offset(&self) -> u64; +} + +/// Wraps a reader to track the current offset +struct OffsetReader<'a, T: 'a> { + reader: &'a mut T, + offset: u64, +} + +impl<'a, T> OffsetReader<'a, T> { + fn new(reader: &'a mut T) -> Self { + Self { reader, offset: 0 } + } +} + +impl<'a, T> Offset for OffsetReader<'a, T> { + fn offset(&self) -> u64 { + self.offset + } +} + +impl<'a, T: Read> Read for OffsetReader<'a, T> { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let bytes_read = self.reader.read(buf)?; + self.offset = self + .offset + .checked_add(bytes_read.to_u64()) + .expect("total bytes read too large for offset type"); + Ok(bytes_read) + } +} + +pub type TryVec = fallible_collections::TryVec; +pub type TryString = fallible_collections::TryVec; +pub type TryHashMap = fallible_collections::TryHashMap; +pub type TryBox = fallible_collections::TryBox; + +// To ensure we don't use stdlib allocating types by accident +#[allow(dead_code)] +struct Vec; +#[allow(dead_code)] +struct Box; +#[allow(dead_code)] +struct HashMap; +#[allow(dead_code)] +struct String; + +/// The return value to the C API +/// Any detail that needs to be communicated to the caller must be encoded here +/// since the [`Error`] type's associated data is part of the FFI. +#[repr(C)] +#[derive(PartialEq, Debug)] +pub enum Status { + Ok = 0, + BadArg = 1, + Invalid = 2, + Unsupported = 3, + Eof = 4, + Io = 5, + Oom = 6, + UnsupportedA1lx, + UnsupportedA1op, + UnsupportedClap, + UnsupportedGrid, + UnsupportedIpro, + UnsupportedLsel, +} + +/// For convenience of creating an error for an unsupported feature which we +/// want to communicate the specific feature back to the C API caller +impl From for Error { + fn from(parse_status: Status) -> Self { + let msg = match parse_status { + Status::Ok + | Status::BadArg + | Status::Invalid + | Status::Unsupported + | Status::Eof + | Status::Io + | Status::Oom => { + panic!("Status -> Error is only for Status:UnsupportedXXX errors") + } + + Status::UnsupportedA1lx => "AV1 layered image indexing (a1lx) is unsupported", + Status::UnsupportedA1op => "Operating point selection (a1op) is unsupported", + Status::UnsupportedClap => "Clean aperture (clap) transform is unsupported", + Status::UnsupportedGrid => "Grid-based images are unsupported", + Status::UnsupportedIpro => "Item protection (ipro) is unsupported", + Status::UnsupportedLsel => "Layer selection (lsel) is unsupported", + }; + Self::UnsupportedDetail(parse_status, msg) + } +} + +impl From for Status { + fn from(error: Error) -> Self { + match error { + Error::NoMoov | Error::InvalidData(_) => Self::Invalid, + Error::Unsupported(_) => Self::Unsupported, + Error::UnsupportedDetail(parse_status, _msg) => parse_status, + Error::UnexpectedEOF => Self::Eof, + Error::Io(_) => { + // Getting std::io::ErrorKind::UnexpectedEof is normal + // but our From trait implementation should have converted + // those to our Error::UnexpectedEOF variant. + Self::Io + } + Error::OutOfMemory => Self::Oom, + } + } +} + +impl From> for Status { + fn from(result: Result<(), Status>) -> Self { + match result { + Ok(()) => Status::Ok, + Err(Status::Ok) => unreachable!(), + Err(e) => e, + } + } +} + +impl From for Status { + fn from(_: fallible_collections::TryReserveError) -> Self { + Status::Oom + } +} + +/// Describes parser failures. +/// +/// This enum wraps the standard `io::Error` type, unified with +/// our own parser error states and those of crates we use. +#[derive(Debug)] +pub enum Error { + /// Parse error caused by corrupt or malformed data. + InvalidData(&'static str), + /// Parse error caused by limited parser support rather than invalid data. + Unsupported(&'static str), + /// Similar to [`Self::Unsupported`], but for errors that have a specific + /// [`Status`] variant for communicating the detail across FFI. + /// See the helper [`From for Error`](enum.Error.html#impl-From) + UnsupportedDetail(Status, &'static str), + /// Reflect `std::io::ErrorKind::UnexpectedEof` for short data. + UnexpectedEOF, + /// Propagate underlying errors from `std::io`. + Io(std::io::Error), + /// read_mp4 terminated without detecting a moov box. + NoMoov, + /// Out of memory + OutOfMemory, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl std::error::Error for Error {} + +impl From for Error { + fn from(_: bitreader::BitReaderError) -> Error { + Error::InvalidData("invalid data") + } +} + +impl From for Error { + fn from(err: std::io::Error) -> Error { + match err.kind() { + std::io::ErrorKind::UnexpectedEof => Error::UnexpectedEOF, + _ => Error::Io(err), + } + } +} + +impl From for Error { + fn from(_: std::string::FromUtf8Error) -> Error { + Error::InvalidData("invalid utf8") + } +} + +impl From for Error { + fn from(_: std::str::Utf8Error) -> Error { + Error::InvalidData("invalid utf8") + } +} + +impl From for Error { + fn from(_: std::num::TryFromIntError) -> Error { + Error::Unsupported("integer conversion failed") + } +} + +impl From for std::io::Error { + fn from(err: Error) -> Self { + let kind = match err { + Error::InvalidData(_) => std::io::ErrorKind::InvalidData, + Error::UnexpectedEOF => std::io::ErrorKind::UnexpectedEof, + Error::Io(io_err) => return io_err, + _ => std::io::ErrorKind::Other, + }; + Self::new(kind, err) + } +} + +impl From for Error { + fn from(_: TryReserveError) -> Error { + Error::OutOfMemory + } +} + +/// Result shorthand using our Error enum. +pub type Result = std::result::Result; + +/// Basic ISO box structure. +/// +/// mp4 files are a sequence of possibly-nested 'box' structures. Each box +/// begins with a header describing the length of the box's data and a +/// four-byte box type which identifies the type of the box. Together these +/// are enough to interpret the contents of that section of the file. +/// +/// See ISOBMFF (ISO 14496-12:2020) § 4.2 +#[derive(Debug, Clone, Copy)] +struct BoxHeader { + /// Box type. + name: BoxType, + /// Size of the box in bytes. + size: u64, + /// Offset to the start of the contained data (or header size). + offset: u64, + /// Uuid for extended type. + #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340 + uuid: Option<[u8; 16]>, +} + +impl BoxHeader { + const MIN_SIZE: u64 = 8; // 4-byte size + 4-byte type + const MIN_LARGE_SIZE: u64 = 16; // 4-byte size + 4-byte type + 16-byte size +} + +/// File type box 'ftyp'. +#[derive(Debug)] +struct FileTypeBox { + #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340 + major_brand: FourCC, + #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340 + minor_version: u32, + compatible_brands: TryVec, +} + +/// Movie header box 'mvhd'. +#[derive(Debug)] +struct MovieHeaderBox { + pub timescale: u32, + #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340 + duration: u64, +} + +#[derive(Debug, Clone, Copy)] +pub struct Matrix { + pub a: i32, // 16.16 fix point + pub b: i32, // 16.16 fix point + pub u: i32, // 2.30 fix point + pub c: i32, // 16.16 fix point + pub d: i32, // 16.16 fix point + pub v: i32, // 2.30 fix point + pub x: i32, // 16.16 fix point + pub y: i32, // 16.16 fix point + pub w: i32, // 2.30 fix point +} + +/// Track header box 'tkhd' +#[derive(Debug, Clone)] +pub struct TrackHeaderBox { + track_id: u32, + pub disabled: bool, + pub duration: u64, + pub width: u32, + pub height: u32, + pub matrix: Matrix, +} + +/// Edit list box 'elst' +#[derive(Debug)] +struct EditListBox { + edits: TryVec, +} + +#[derive(Debug)] +struct Edit { + segment_duration: u64, + media_time: i64, + #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340 + media_rate_integer: i16, + #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340 + media_rate_fraction: i16, +} + +/// Media header box 'mdhd' +#[derive(Debug)] +struct MediaHeaderBox { + timescale: u32, + duration: u64, +} + +// Chunk offset box 'stco' or 'co64' +#[derive(Debug)] +pub struct ChunkOffsetBox { + pub offsets: TryVec, +} + +// Sync sample box 'stss' +#[derive(Debug)] +pub struct SyncSampleBox { + pub samples: TryVec, +} + +// Sample to chunk box 'stsc' +#[derive(Debug)] +pub struct SampleToChunkBox { + pub samples: TryVec, +} + +#[derive(Debug)] +pub struct SampleToChunk { + pub first_chunk: u32, + pub samples_per_chunk: u32, + pub sample_description_index: u32, +} + +// Sample size box 'stsz' +#[derive(Debug)] +pub struct SampleSizeBox { + pub sample_size: u32, + pub sample_sizes: TryVec, +} + +// Time to sample box 'stts' +#[derive(Debug)] +pub struct TimeToSampleBox { + pub samples: TryVec, +} + +#[repr(C)] +#[derive(Debug)] +pub struct Sample { + pub sample_count: u32, + pub sample_delta: u32, +} + +#[derive(Debug, Clone, Copy)] +pub enum TimeOffsetVersion { + Version0(u32), + Version1(i32), +} + +#[derive(Debug, Clone)] +pub struct TimeOffset { + pub sample_count: u32, + pub time_offset: TimeOffsetVersion, +} + +#[derive(Debug)] +pub struct CompositionOffsetBox { + pub samples: TryVec, +} + +// Handler reference box 'hdlr' +#[derive(Debug)] +struct HandlerBox { + handler_type: FourCC, +} + +// Sample description box 'stsd' +#[derive(Debug)] +pub struct SampleDescriptionBox { + pub descriptions: TryVec, +} + +#[derive(Debug)] +pub enum SampleEntry { + Audio(AudioSampleEntry), + Video(VideoSampleEntry), + Unknown, +} + +/// An Elementary Stream Descriptor +/// See MPEG-4 Systems (ISO 14496-1:2010) § 7.2.6.5 +#[allow(non_camel_case_types)] +#[derive(Debug, Default)] +pub struct ES_Descriptor { + pub audio_codec: CodecType, + pub audio_object_type: Option, + pub extended_audio_object_type: Option, + pub audio_sample_rate: Option, + pub audio_channel_count: Option, + #[cfg(feature = "mp4v")] + pub video_codec: CodecType, + pub codec_esds: TryVec, + pub decoder_specific_data: TryVec, // Data in DECODER_SPECIFIC_TAG +} + +#[allow(non_camel_case_types)] +#[derive(Debug)] +pub enum AudioCodecSpecific { + ES_Descriptor(ES_Descriptor), + FLACSpecificBox(FLACSpecificBox), + OpusSpecificBox(OpusSpecificBox), + ALACSpecificBox(ALACSpecificBox), + MP3, + LPCM, + #[cfg(feature = "3gpp")] + AMRSpecificBox(TryVec), +} + +#[derive(Debug)] +pub struct AudioSampleEntry { + pub codec_type: CodecType, + #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340 + data_reference_index: u16, + pub channelcount: u32, + pub samplesize: u16, + pub samplerate: f64, + pub codec_specific: AudioCodecSpecific, + pub protection_info: TryVec, +} + +#[derive(Debug)] +pub enum VideoCodecSpecific { + AVCConfig(TryVec), + VPxConfig(VPxConfigBox), + AV1Config(AV1ConfigBox), + ESDSConfig(TryVec), + H263Config(TryVec), +} + +#[derive(Debug)] +pub struct VideoSampleEntry { + pub codec_type: CodecType, + #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340 + data_reference_index: u16, + pub width: u16, + pub height: u16, + pub codec_specific: VideoCodecSpecific, + pub protection_info: TryVec, +} + +/// Represent a Video Partition Codec Configuration 'vpcC' box (aka vp9). The meaning of each +/// field is covered in detail in "VP Codec ISO Media File Format Binding". +#[derive(Debug)] +pub struct VPxConfigBox { + /// An integer that specifies the VP codec profile. + #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340 + profile: u8, + /// An integer that specifies a VP codec level all samples conform to the following table. + /// For a description of the various levels, please refer to the VP9 Bitstream Specification. + #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340 + level: u8, + /// An integer that specifies the bit depth of the luma and color components. Valid values + /// are 8, 10, and 12. + pub bit_depth: u8, + /// Really an enum defined by the "Colour primaries" section of ISO 23091-2:2019 § 8.1. + pub colour_primaries: u8, + /// Really an enum defined by "VP Codec ISO Media File Format Binding". + pub chroma_subsampling: u8, + /// Really an enum defined by the "Transfer characteristics" section of ISO 23091-2:2019 § 8.2. + #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340 + transfer_characteristics: u8, + /// Really an enum defined by the "Matrix coefficients" section of ISO 23091-2:2019 § 8.3. + /// Available in 'VP Codec ISO Media File Format' version 1 only. + #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340 + matrix_coefficients: Option, + /// Indicates the black level and range of the luma and chroma signals. 0 = legal range + /// (e.g. 16-235 for 8 bit sample depth); 1 = full range (e.g. 0-255 for 8-bit sample depth). + #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340 + video_full_range_flag: bool, + /// This is not used for VP8 and VP9 . Intended for binary codec initialization data. + pub codec_init: TryVec, +} + +/// See [AV1-ISOBMFF § 2.3.3](https://aomediacodec.github.io/av1-isobmff/#av1codecconfigurationbox-syntax) +#[derive(Debug)] +pub struct AV1ConfigBox { + pub profile: u8, + pub level: u8, + pub tier: u8, + pub bit_depth: u8, + pub monochrome: bool, + pub chroma_subsampling_x: u8, + pub chroma_subsampling_y: u8, + pub chroma_sample_position: u8, + pub initial_presentation_delay_present: bool, + pub initial_presentation_delay_minus_one: u8, + // The raw config contained in the av1c box. Because some decoders accept this data as a binary + // blob, rather than as structured data, we store the blob here for convenience. + pub raw_config: TryVec, +} + +impl AV1ConfigBox { + const CONFIG_OBUS_OFFSET: usize = 4; + + pub fn config_obus(&self) -> &[u8] { + &self.raw_config[Self::CONFIG_OBUS_OFFSET..] + } +} + +#[derive(Debug)] +pub struct FLACMetadataBlock { + pub block_type: u8, + pub data: TryVec, +} + +/// Represents a FLACSpecificBox 'dfLa' +#[derive(Debug)] +pub struct FLACSpecificBox { + #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340 + version: u8, + pub blocks: TryVec, +} + +#[derive(Debug)] +struct ChannelMappingTable { + stream_count: u8, + coupled_count: u8, + channel_mapping: TryVec, +} + +/// Represent an OpusSpecificBox 'dOps' +#[derive(Debug)] +pub struct OpusSpecificBox { + pub version: u8, + output_channel_count: u8, + pre_skip: u16, + input_sample_rate: u32, + output_gain: i16, + channel_mapping_family: u8, + channel_mapping_table: Option, +} + +/// Represent an ALACSpecificBox 'alac' +#[derive(Debug)] +pub struct ALACSpecificBox { + #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340 + version: u8, + pub data: TryVec, +} + +#[derive(Debug)] +pub struct MovieExtendsBox { + pub fragment_duration: Option, +} + +pub type ByteData = TryVec; + +#[derive(Debug, Default)] +pub struct ProtectionSystemSpecificHeaderBox { + pub system_id: ByteData, + pub kid: TryVec, + pub data: ByteData, + + // The entire pssh box (include header) required by Gecko. + pub box_content: ByteData, +} + +#[derive(Debug, Default, Clone)] +pub struct SchemeTypeBox { + pub scheme_type: FourCC, + pub scheme_version: u32, +} + +#[derive(Debug, Default)] +pub struct TrackEncryptionBox { + pub is_encrypted: u8, + pub iv_size: u8, + pub kid: TryVec, + // Members for pattern encryption schemes + pub crypt_byte_block_count: Option, + pub skip_byte_block_count: Option, + pub constant_iv: Option>, + // End pattern encryption scheme members +} + +#[derive(Debug, Default)] +pub struct ProtectionSchemeInfoBox { + pub original_format: FourCC, + pub scheme_type: Option, + pub tenc: Option, +} + +/// Represents a userdata box 'udta'. +/// Currently, only the metadata atom 'meta' +/// is parsed. +#[derive(Debug, Default)] +pub struct UserdataBox { + pub meta: Option, +} + +/// Represents possible contents of the +/// ©gen or gnre atoms within a metadata box. +/// 'udta.meta.ilst' may only have either a +/// standard genre box 'gnre' or a custom +/// genre box '©gen', but never both at once. +#[derive(Debug, PartialEq)] +pub enum Genre { + /// A standard ID3v1 numbered genre. + StandardGenre(u8), + /// Any custom genre string. + CustomGenre(TryString), +} + +/// Represents the contents of a 'stik' +/// atom that indicates content types within +/// iTunes. +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum MediaType { + /// Movie is stored as 0 in a 'stik' atom. + Movie, // 0 + /// Normal is stored as 1 in a 'stik' atom. + Normal, // 1 + /// AudioBook is stored as 2 in a 'stik' atom. + AudioBook, // 2 + /// WhackedBookmark is stored as 5 in a 'stik' atom. + WhackedBookmark, // 5 + /// MusicVideo is stored as 6 in a 'stik' atom. + MusicVideo, // 6 + /// ShortFilm is stored as 9 in a 'stik' atom. + ShortFilm, // 9 + /// TVShow is stored as 10 in a 'stik' atom. + TVShow, // 10 + /// Booklet is stored as 11 in a 'stik' atom. + Booklet, // 11 + /// An unknown 'stik' value. + Unknown(u8), +} + +/// Represents the parental advisory rating on the track, +/// stored within the 'rtng' atom. +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum AdvisoryRating { + /// Clean is always stored as 2 in an 'rtng' atom. + Clean, // 2 + /// A value of 0 in an 'rtng' atom indicates 'Inoffensive' + Inoffensive, // 0 + /// Any non 2 or 0 value in 'rtng' indicates the track is explicit. + Explicit(u8), +} + +/// Represents the contents of 'ilst' atoms within +/// a metadata box 'meta', parsed as iTunes metadata using +/// the conventional tags. +#[derive(Debug, Default)] +pub struct MetadataBox { + /// The album name, '©alb' + pub album: Option, + /// The artist name '©art' or '©ART' + pub artist: Option, + /// The album artist 'aART' + pub album_artist: Option, + /// Track comments '©cmt' + pub comment: Option, + /// The date or year field '©day' + /// + /// This is stored as an arbitrary string, + /// and may not necessarily be in a valid date + /// format. + pub year: Option, + /// The track title '©nam' + pub title: Option, + /// The track genre '©gen' or 'gnre'. + pub genre: Option, + /// The track number 'trkn'. + pub track_number: Option, + /// The disc number 'disk' + pub disc_number: Option, + /// The total number of tracks on the disc, + /// stored in 'trkn' + pub total_tracks: Option, + /// The total number of discs in the album, + /// stored in 'disk' + pub total_discs: Option, + /// The composer of the track '©wrt' + pub composer: Option, + /// The encoder used to create this track '©too' + pub encoder: Option, + /// The encoded-by settingo this track '©enc' + pub encoded_by: Option, + /// The tempo or BPM of the track 'tmpo' + pub beats_per_minute: Option, + /// Copyright information of the track 'cprt' + pub copyright: Option, + /// Whether or not this track is part of a compilation 'cpil' + pub compilation: Option, + /// The advisory rating of this track 'rtng' + pub advisory: Option, + /// The personal rating of this track, 'rate'. + /// + /// This is stored in the box as string data, but + /// the format is an integer percentage from 0 - 100, + /// where 100 is displayed as 5 stars out of 5. + pub rating: Option, + /// The grouping this track belongs to '©grp' + pub grouping: Option, + /// The media type of this track 'stik' + pub media_type: Option, // stik + /// Whether or not this track is a podcast 'pcst' + pub podcast: Option, + /// The category of ths track 'catg' + pub category: Option, + /// The podcast keyword 'keyw' + pub keyword: Option, + /// The podcast url 'purl' + pub podcast_url: Option, + /// The podcast episode GUID 'egid' + pub podcast_guid: Option, + /// The description of the track 'desc' + pub description: Option, + /// The long description of the track 'ldes'. + /// + /// Unlike other string fields, the long description field + /// can be longer than 256 characters. + pub long_description: Option, + /// The lyrics of the track '©lyr'. + /// + /// Unlike other string fields, the lyrics field + /// can be longer than 256 characters. + pub lyrics: Option, + /// The name of the TV network this track aired on 'tvnn'. + pub tv_network_name: Option, + /// The name of the TV Show for this track 'tvsh'. + pub tv_show_name: Option, + /// The name of the TV Episode for this track 'tven'. + pub tv_episode_name: Option, + /// The number of the TV Episode for this track 'tves'. + pub tv_episode_number: Option, + /// The season of the TV Episode of this track 'tvsn'. + pub tv_season: Option, + /// The date this track was purchased 'purd'. + pub purchase_date: Option, + /// Whether or not this track supports gapless playback 'pgap' + pub gapless_playback: Option, + /// Any cover artwork attached to this track 'covr' + /// + /// 'covr' is unique in that it may contain multiple 'data' sub-entries, + /// each an image file. Here, each subentry's raw binary data is exposed, + /// which may contain image data in JPEG or PNG format. + pub cover_art: Option>>, + /// The owner of the track 'ownr' + pub owner: Option, + /// Whether or not this track is HD Video 'hdvd' + pub hd_video: Option, + /// The name of the track to sort by 'sonm' + pub sort_name: Option, + /// The name of the album to sort by 'soal' + pub sort_album: Option, + /// The name of the artist to sort by 'soar' + pub sort_artist: Option, + /// The name of the album artist to sort by 'soaa' + pub sort_album_artist: Option, + /// The name of the composer to sort by 'soco' + pub sort_composer: Option, + /// Metadata + #[cfg(feature = "meta-xml")] + pub xml: Option, +} + +/// See ISOBMFF (ISO 14496-12:2020) § 8.11.2.1 +#[cfg(feature = "meta-xml")] +#[derive(Debug)] +pub enum XmlBox { + /// XML metadata + StringXmlBox(TryString), + /// Binary XML metadata + BinaryXmlBox(TryVec), +} + +/// Internal data structures. +#[derive(Debug, Default)] +pub struct MediaContext { + pub timescale: Option, + /// Tracks found in the file. + pub tracks: TryVec, + pub mvex: Option, + pub psshs: TryVec, + pub userdata: Option>, + #[cfg(feature = "meta-xml")] + pub metadata: Option>, +} + +/// An ISOBMFF item as described by an iloc box. For the sake of avoiding copies, +/// this can either be represented by the `Location` variant, which indicates +/// where the data exists within a `MediaDataBox` stored separately, or the +/// `Data` variant which owns the data. Unfortunately, it's not simple to +/// represent this as a [`std::borrow::Cow`], or other reference-based type, because +/// multiple instances may references different parts of the same [`MediaDataBox`] +/// and we want to avoid the copy that splitting the storage would entail. +#[derive(Debug)] +enum IsobmffItem { + Location(Extent), + Data(TryVec), +} + +#[derive(Debug)] +struct AvifItem { + /// The `item_ID` from ISOBMFF (ISO 14496-12:2020) § 8.11.3 + /// + /// See [`read_iloc`] + id: ItemId, + + /// AV1 Image Item per + image_data: IsobmffItem, +} + +impl AvifItem { + fn with_data_location(id: ItemId, extent: Extent) -> Self { + Self { + id, + image_data: IsobmffItem::Location(extent), + } + } + + fn with_inline_data(id: ItemId) -> Self { + Self { + id, + image_data: IsobmffItem::Data(TryVec::new()), + } + } +} + +#[derive(Debug)] +pub struct AvifContext { + /// Level of deviation from the specification before failing the parse + strictness: ParseStrictness, + /// Referred to by the `Location` variants of the `AvifItem`s in this struct + item_storage: TryVec, + /// The item indicated by the `pitm` box, See ISOBMFF (ISO 14496-12:2020) § 8.11.4 + primary_item: AvifItem, + /// Associated alpha channel for the primary item, if any + alpha_item: Option, + /// If true, divide RGB values by the alpha value. + /// See `prem` in MIAF (ISO 23000-22:2019) § 7.3.5.2 + pub premultiplied_alpha: bool, + /// All properties associated with `primary_item` or `alpha_item` + item_properties: ItemPropertiesBox, +} + +impl AvifContext { + pub fn primary_item_coded_data(&self) -> &[u8] { + self.item_as_slice(&self.primary_item) + } + + pub fn primary_item_bits_per_channel(&self) -> Result<&[u8]> { + self.image_bits_per_channel(self.primary_item.id) + } + + pub fn alpha_item_coded_data(&self) -> &[u8] { + self.alpha_item + .as_ref() + .map_or(&[], |item| self.item_as_slice(item)) + } + + pub fn alpha_item_bits_per_channel(&self) -> Result<&[u8]> { + self.alpha_item + .as_ref() + .map_or(Ok(&[]), |item| self.image_bits_per_channel(item.id)) + } + + fn image_bits_per_channel(&self, item_id: ItemId) -> Result<&[u8]> { + match self + .item_properties + .get(item_id, BoxType::PixelInformationBox)? + { + Some(ItemProperty::Channels(pixi)) => Ok(pixi.bits_per_channel.as_slice()), + Some(other_property) => panic!("property key mismatch: {:?}", other_property), + None => Ok(&[]), + } + } + + pub fn spatial_extents_ptr(&self) -> Result<*const ImageSpatialExtentsProperty> { + match self + .item_properties + .get(self.primary_item.id, BoxType::ImageSpatialExtentsProperty)? + { + Some(ItemProperty::ImageSpatialExtents(ispe)) => Ok(ispe), + Some(other_property) => panic!("property key mismatch: {:?}", other_property), + None => { + fail_if( + self.strictness == ParseStrictness::Permissive, + "ispe is a mandatory property", + )?; + Ok(std::ptr::null()) + } + } + } + + pub fn nclx_colour_information_ptr(&self) -> Result<*const NclxColourInformation> { + let nclx_colr_boxes = self + .item_properties + .get_multiple(self.primary_item.id, |prop| { + matches!(prop, ItemProperty::Colour(ColourInformation::Nclx(_))) + })?; + + match *nclx_colr_boxes.as_slice() { + [] => Ok(std::ptr::null()), + [ItemProperty::Colour(ColourInformation::Nclx(nclx)), ..] => { + if nclx_colr_boxes.len() > 1 { + warn!("Multiple nclx colr boxes, using first"); + } + Ok(nclx) + } + _ => unreachable!("Expect only ColourInformation::Nclx(_) matches"), + } + } + + pub fn icc_colour_information(&self) -> Result<&[u8]> { + let icc_colr_boxes = self + .item_properties + .get_multiple(self.primary_item.id, |prop| { + matches!(prop, ItemProperty::Colour(ColourInformation::Icc(_, _))) + })?; + + match *icc_colr_boxes.as_slice() { + [] => Ok(&[]), + [ItemProperty::Colour(ColourInformation::Icc(icc, _)), ..] => { + if icc_colr_boxes.len() > 1 { + warn!("Multiple ICC profiles in colr boxes, using first"); + } + Ok(icc.bytes.as_slice()) + } + _ => unreachable!("Expect only ColourInformation::Icc(_) matches"), + } + } + + pub fn image_rotation(&self) -> Result { + match self + .item_properties + .get(self.primary_item.id, BoxType::ImageRotation)? + { + Some(ItemProperty::Rotation(irot)) => Ok(*irot), + Some(other_property) => panic!("property key mismatch: {:?}", other_property), + None => Ok(ImageRotation::D0), + } + } + + pub fn image_mirror_ptr(&self) -> Result<*const ImageMirror> { + match self + .item_properties + .get(self.primary_item.id, BoxType::ImageMirror)? + { + Some(ItemProperty::Mirroring(imir)) => Ok(imir), + Some(other_property) => panic!("property key mismatch: {:?}", other_property), + None => Ok(std::ptr::null()), + } + } + + /// A helper for the various `AvifItem`s to expose a reference to the + /// underlying data while avoiding copies. + fn item_as_slice<'a>(&'a self, item: &'a AvifItem) -> &'a [u8] { + match &item.image_data { + IsobmffItem::Location(extent) => { + for mdat in &self.item_storage { + if let Some(slice) = mdat.get(extent) { + return slice; + } + } + unreachable!( + "IsobmffItem::Location requires the location exists in AvifContext::item_storage" + ); + } + IsobmffItem::Data(data) => data.as_slice(), + } + } +} + +struct AvifMeta { + item_references: TryVec, + item_properties: ItemPropertiesBox, + primary_item_id: ItemId, + iloc_items: TryHashMap, +} + +/// A Media Data Box +/// See ISOBMFF (ISO 14496-12:2020) § 8.1.1 +struct MediaDataBox { + /// Offset of `data` from the beginning of the "file". See ConstructionMethod::File. + /// Note: the file may not be an actual file, read_avif supports any `&mut impl Read` + /// source for input. However we try to match the terminology used in the spec. + file_offset: u64, + data: TryVec, +} + +impl fmt::Debug for MediaDataBox { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("MediaDataBox") + .field("file_offset", &self.file_offset) + .field("data", &format_args!("{} bytes", self.data.len())) + .finish() + } +} + +impl MediaDataBox { + /// Convert an absolute offset to an offset relative to the beginning of the + /// `self.data` field. Returns None if the offset would be negative. + /// + /// # Panics + /// + /// Panics if the offset would overflow a `usize`. + fn file_offset_to_data_offset(&self, offset: u64) -> Option { + let start = offset + .checked_sub(self.file_offset)? + .try_into() + .expect("usize overflow"); + Some(start) + } + + /// Return a slice from the MediaDataBox specified by the provided `extent`. + /// Returns `None` if the extent isn't fully contained by the MediaDataBox. + /// + /// # Panics + /// + /// Panics if either the offset or length (if the extent is bounded) of the + /// slice would overflow a `usize`. + pub fn get<'a>(&'a self, extent: &'a Extent) -> Option<&'a [u8]> { + match extent { + Extent::WithLength { offset, len } => { + let start = self.file_offset_to_data_offset(*offset)?; + let end = start.checked_add(*len).expect("usize overflow"); + self.data.get(start..end) + } + Extent::ToEnd { offset } => { + let start = self.file_offset_to_data_offset(*offset)?; + self.data.get(start..) + } + } + } +} + +#[cfg(test)] +mod media_data_box_tests { + use super::*; + + impl MediaDataBox { + fn at_offset(file_offset: u64, data: std::vec::Vec) -> Self { + MediaDataBox { + file_offset, + data: data.into(), + } + } + } + + #[test] + fn extent_with_length_before_mdat_returns_none() { + let mdat = MediaDataBox::at_offset(100, vec![1; 5]); + let extent = Extent::WithLength { offset: 0, len: 2 }; + + assert!(mdat.get(&extent).is_none()); + } + + #[test] + fn extent_to_end_before_mdat_returns_none() { + let mdat = MediaDataBox::at_offset(100, vec![1; 5]); + let extent = Extent::ToEnd { offset: 0 }; + + assert!(mdat.get(&extent).is_none()); + } + + #[test] + fn extent_with_length_crossing_front_mdat_boundary_returns_none() { + let mdat = MediaDataBox::at_offset(100, vec![1; 5]); + let extent = Extent::WithLength { offset: 99, len: 3 }; + + assert!(mdat.get(&extent).is_none()); + } + + #[test] + fn extent_with_length_which_is_subset_of_mdat() { + let mdat = MediaDataBox::at_offset(100, vec![1; 5]); + let extent = Extent::WithLength { + offset: 101, + len: 2, + }; + + assert_eq!(mdat.get(&extent), Some(&[1, 1][..])); + } + + #[test] + fn extent_to_end_which_is_subset_of_mdat() { + let mdat = MediaDataBox::at_offset(100, vec![1; 5]); + let extent = Extent::ToEnd { offset: 101 }; + + assert_eq!(mdat.get(&extent), Some(&[1, 1, 1, 1][..])); + } + + #[test] + fn extent_with_length_which_is_all_of_mdat() { + let mdat = MediaDataBox::at_offset(100, vec![1; 5]); + let extent = Extent::WithLength { + offset: 100, + len: 5, + }; + + assert_eq!(mdat.get(&extent), Some(mdat.data.as_slice())); + } + + #[test] + fn extent_to_end_which_is_all_of_mdat() { + let mdat = MediaDataBox::at_offset(100, vec![1; 5]); + let extent = Extent::ToEnd { offset: 100 }; + + assert_eq!(mdat.get(&extent), Some(mdat.data.as_slice())); + } + + #[test] + fn extent_with_length_crossing_back_mdat_boundary_returns_none() { + let mdat = MediaDataBox::at_offset(100, vec![1; 5]); + let extent = Extent::WithLength { + offset: 103, + len: 3, + }; + + assert!(mdat.get(&extent).is_none()); + } + + #[test] + fn extent_with_length_after_mdat_returns_none() { + let mdat = MediaDataBox::at_offset(100, vec![1; 5]); + let extent = Extent::WithLength { + offset: 200, + len: 2, + }; + + assert!(mdat.get(&extent).is_none()); + } + + #[test] + fn extent_to_end_after_mdat_returns_none() { + let mdat = MediaDataBox::at_offset(100, vec![1; 5]); + let extent = Extent::ToEnd { offset: 200 }; + + assert!(mdat.get(&extent).is_none()); + } + + #[test] + #[should_panic(expected = "usize overflow")] + fn extent_with_length_which_overflows_usize_panics() { + let mdat = MediaDataBox::at_offset(std::u64::MAX - 1, vec![1; 5]); + let extent = Extent::WithLength { + offset: std::u64::MAX, + len: std::usize::MAX, + }; + + mdat.get(&extent); + } + + // The end of the range would overflow `usize` if it were calculated, but + // because the range end is unbounded, we don't calculate it. + #[test] + fn extent_to_end_which_overflows_usize() { + let mdat = MediaDataBox::at_offset(std::u64::MAX - 1, vec![1; 5]); + let extent = Extent::ToEnd { + offset: std::u64::MAX, + }; + + assert_eq!(mdat.get(&extent), Some(&[1, 1, 1, 1][..])); + } +} + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +struct PropertyIndex(u16); +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd)] +struct ItemId(u32); + +impl ItemId { + fn read(src: &mut impl ReadBytesExt, version: u8) -> Result { + Ok(ItemId(if version == 0 { + be_u16(src)?.into() + } else { + be_u32(src)? + })) + } +} + +/// Used for 'infe' boxes within 'iinf' boxes +/// See ISOBMFF (ISO 14496-12:2020) § 8.11.6 +/// Only versions {2, 3} are supported +#[derive(Debug)] +struct ItemInfoEntry { + item_id: ItemId, + item_type: u32, +} + +/// See ISOBMFF (ISO 14496-12:2020) § 8.11.12 +#[derive(Debug)] +struct SingleItemTypeReferenceBox { + item_type: FourCC, + from_item_id: ItemId, + to_item_id: ItemId, +} + +/// Potential sizes (in bytes) of variable-sized fields of the 'iloc' box +/// See ISOBMFF (ISO 14496-12:2020) § 8.11.3 +#[derive(Debug, Clone, Copy, PartialEq)] +enum IlocFieldSize { + Zero, + Four, + Eight, +} + +impl IlocFieldSize { + fn as_bits(&self) -> u8 { + match self { + IlocFieldSize::Zero => 0, + IlocFieldSize::Four => 32, + IlocFieldSize::Eight => 64, + } + } +} + +impl TryFrom for IlocFieldSize { + type Error = Error; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Zero), + 4 => Ok(Self::Four), + 8 => Ok(Self::Eight), + _ => Err(Error::InvalidData("value must be in the set {0, 4, 8}")), + } + } +} + +#[derive(PartialEq)] +enum IlocVersion { + Zero, + One, + Two, +} + +impl TryFrom for IlocVersion { + type Error = Error; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Zero), + 1 => Ok(Self::One), + 2 => Ok(Self::Two), + _ => Err(Error::Unsupported("unsupported version in 'iloc' box")), + } + } +} + +/// Used for 'iloc' boxes +/// See ISOBMFF (ISO 14496-12:2020) § 8.11.3 +/// `base_offset` is omitted since it is integrated into the ranges in `extents` +/// `data_reference_index` is omitted, since only 0 (i.e., this file) is supported +#[derive(Debug)] +struct ItemLocationBoxItem { + construction_method: ConstructionMethod, + /// Unused for ConstructionMethod::Idat + extents: TryVec, +} + +/// See ISOBMFF (ISO 14496-12:2020) § 8.11.3 +/// +/// Note: per MIAF (ISO 23000-22:2019) § 7.2.1.7:
+/// > MIAF image items are constrained as follows:
+/// > — `construction_method` shall be equal to 0 for MIAF image items that are coded image items.
+/// > — `construction_method` shall be equal to 0 or 1 for MIAF image items that are derived image items. +#[derive(Clone, Copy, Debug, PartialEq)] +enum ConstructionMethod { + File = 0, + Idat = 1, + #[allow(dead_code)] // TODO: see https://github.com/mozilla/mp4parse-rust/issues/196 + Item = 2, +} + +/// Describes a region where a item specified by an `ItemLocationBoxItem` is stored. +/// The offset is `u64` since that's the maximum possible size and since the relative +/// nature of `MediaDataBox` means this can still possibly succeed even in the case +/// that the raw value exceeds std::usize::MAX on platforms where that type is smaller +/// than u64. However, `len` is stored as a `usize` since no value larger than +/// `std::usize::MAX` can be used in a successful indexing operation in rust. +/// `extent_index` is omitted since it's only used for ConstructionMethod::Item which +/// is currently not implemented. +#[derive(Clone, Debug)] +enum Extent { + WithLength { offset: u64, len: usize }, + ToEnd { offset: u64 }, +} + +#[derive(Debug, PartialEq)] +pub enum TrackType { + Audio, + Video, + Metadata, + Unknown, +} + +impl Default for TrackType { + fn default() -> Self { + TrackType::Unknown + } +} + +// This type is used by mp4parse_capi since it needs to be passed from FFI consumers +// The C-visible struct is renamed via mp4parse_capi/cbindgen.toml to match naming conventions +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum ParseStrictness { + Permissive, // Error only on ambiguous inputs + Normal, // Error on "shall" directives, log warnings for "should" + Strict, // Error on "should" directives +} + +fn fail_if(violation: bool, message: &'static str) -> Result<()> { + if violation { + Err(Error::InvalidData(message)) + } else { + warn!("{}", message); + Ok(()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum CodecType { + Unknown, + MP3, + AAC, + FLAC, + Opus, + H264, // 14496-10 + MP4V, // 14496-2 + AV1, + VP9, + VP8, + EncryptedVideo, + EncryptedAudio, + LPCM, // QT + ALAC, + H263, + #[cfg(feature = "3gpp")] + AMRNB, + #[cfg(feature = "3gpp")] + AMRWB, +} + +impl Default for CodecType { + fn default() -> Self { + CodecType::Unknown + } +} + +/// The media's global (mvhd) timescale in units per second. +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct MediaTimeScale(pub u64); + +/// A time to be scaled by the media's global (mvhd) timescale. +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct MediaScaledTime(pub u64); + +/// The track's local (mdhd) timescale. +/// Members are timescale units per second and the track id. +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct TrackTimeScale(pub T, pub usize); + +/// A time to be scaled by the track's local (mdhd) timescale. +/// Members are time in scale units and the track id. +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct TrackScaledTime(pub T, pub usize); + +impl std::ops::Add for TrackScaledTime +where + T: num_traits::CheckedAdd, +{ + type Output = Option; + + fn add(self, other: TrackScaledTime) -> Self::Output { + self.0.checked_add(&other.0).map(|sum| Self(sum, self.1)) + } +} + +#[derive(Debug, Default)] +pub struct Track { + pub id: usize, + pub track_type: TrackType, + pub empty_duration: Option, + pub media_time: Option>, + pub timescale: Option>, + pub duration: Option>, + pub track_id: Option, + pub tkhd: Option, // TODO(kinetik): find a nicer way to export this. + pub stsd: Option, + pub stts: Option, + pub stsc: Option, + pub stsz: Option, + pub stco: Option, // It is for stco or co64. + pub stss: Option, + pub ctts: Option, +} + +impl Track { + fn new(id: usize) -> Track { + Track { + id, + ..Default::default() + } + } +} + +/// See ISOBMFF (ISO 14496-12:2020) § 4.2 +struct BMFFBox<'a, T: 'a> { + head: BoxHeader, + content: Take<&'a mut T>, +} + +struct BoxIter<'a, T: 'a> { + src: &'a mut T, +} + +impl<'a, T: Read> BoxIter<'a, T> { + fn new(src: &mut T) -> BoxIter { + BoxIter { src } + } + + fn next_box(&mut self) -> Result>> { + let r = read_box_header(self.src); + match r { + Ok(h) => Ok(Some(BMFFBox { + head: h, + content: self.src.take(h.size - h.offset), + })), + Err(Error::UnexpectedEOF) => Ok(None), + Err(e) => Err(e), + } + } +} + +impl<'a, T: Read> Read for BMFFBox<'a, T> { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.content.read(buf) + } +} + +impl<'a, T: Read> TryRead for BMFFBox<'a, T> { + fn try_read_to_end(&mut self, buf: &mut TryVec) -> std::io::Result { + fallible_collections::try_read_up_to(self, self.bytes_left(), buf) + } +} + +impl<'a, T: Offset> Offset for BMFFBox<'a, T> { + fn offset(&self) -> u64 { + self.content.get_ref().offset() + } +} + +impl<'a, T: Read> BMFFBox<'a, T> { + fn bytes_left(&self) -> u64 { + self.content.limit() + } + + fn get_header(&self) -> &BoxHeader { + &self.head + } + + fn box_iter<'b>(&'b mut self) -> BoxIter> { + BoxIter::new(self) + } +} + +impl<'a, T> Drop for BMFFBox<'a, T> { + fn drop(&mut self) { + if self.content.limit() > 0 { + let name: FourCC = From::from(self.head.name); + debug!("Dropping {} bytes in '{}'", self.content.limit(), name); + } + } +} + +/// Read and parse a box header. +/// +/// Call this first to determine the type of a particular mp4 box +/// and its length. Used internally for dispatching to specific +/// parsers for the internal content, or to get the length to +/// skip unknown or uninteresting boxes. +/// +/// See ISOBMFF (ISO 14496-12:2020) § 4.2 +fn read_box_header(src: &mut T) -> Result { + let size32 = be_u32(src)?; + let name = BoxType::from(be_u32(src)?); + let size = match size32 { + // valid only for top-level box and indicates it's the last box in the file. usually mdat. + 0 => return Err(Error::Unsupported("unknown sized box")), + 1 => { + let size64 = be_u64(src)?; + if size64 < BoxHeader::MIN_LARGE_SIZE { + return Err(Error::InvalidData("malformed wide size")); + } + size64 + } + _ => { + if u64::from(size32) < BoxHeader::MIN_SIZE { + return Err(Error::InvalidData("malformed size")); + } + u64::from(size32) + } + }; + let mut offset = match size32 { + 1 => BoxHeader::MIN_LARGE_SIZE, + _ => BoxHeader::MIN_SIZE, + }; + let uuid = if name == BoxType::UuidBox { + if size >= offset + 16 { + let mut buffer = [0u8; 16]; + let count = src.read(&mut buffer)?; + offset += count.to_u64(); + if count == 16 { + Some(buffer) + } else { + debug!("malformed uuid (short read), skipping"); + None + } + } else { + debug!("malformed uuid, skipping"); + None + } + } else { + None + }; + assert!(offset <= size); + Ok(BoxHeader { + name, + size, + offset, + uuid, + }) +} + +/// Parse the extra header fields for a full box. +fn read_fullbox_extra(src: &mut T) -> Result<(u8, u32)> { + let version = src.read_u8()?; + let flags_a = src.read_u8()?; + let flags_b = src.read_u8()?; + let flags_c = src.read_u8()?; + Ok(( + version, + u32::from(flags_a) << 16 | u32::from(flags_b) << 8 | u32::from(flags_c), + )) +} + +// Parse the extra fields for a full box whose flag fields must be zero. +fn read_fullbox_version_no_flags(src: &mut T) -> Result { + let (version, flags) = read_fullbox_extra(src)?; + + if flags != 0 { + return Err(Error::Unsupported("expected flags to be 0")); + } + + Ok(version) +} + +/// Skip over the entire contents of a box. +fn skip_box_content(src: &mut BMFFBox) -> Result<()> { + // Skip the contents of unknown chunks. + let to_skip = { + let header = src.get_header(); + debug!("{:?} (skipped)", header); + header + .size + .checked_sub(header.offset) + .expect("header offset > size") + }; + assert_eq!(to_skip, src.bytes_left()); + skip(src, to_skip) +} + +/// Skip over the remain data of a box. +fn skip_box_remain(src: &mut BMFFBox) -> Result<()> { + let remain = { + let header = src.get_header(); + let len = src.bytes_left(); + debug!("remain {} (skipped) in {:?}", len, header); + len + }; + skip(src, remain) +} + +/// Read the contents of an AVIF file +pub fn read_avif(f: &mut T, strictness: ParseStrictness) -> Result { + let _ = env_logger::try_init(); + + debug!("read_avif(strictness: {:?})", strictness); + + let mut f = OffsetReader::new(f); + + let mut iter = BoxIter::new(&mut f); + + // 'ftyp' box must occur first; see ISOBMFF (ISO 14496-12:2020) § 4.3.1 + if let Some(mut b) = iter.next_box()? { + if b.head.name == BoxType::FileTypeBox { + let ftyp = read_ftyp(&mut b)?; + if !ftyp.compatible_brands.contains(&MIF1_BRAND) { + // This mandatory inclusion of this brand is in the process of being changed + // to optional. In anticipation of that, only give an error in strict mode + // See https://github.com/MPEGGroup/MIAF/issues/5 + // and https://github.com/MPEGGroup/FileFormat/issues/23 + fail_if( + strictness == ParseStrictness::Strict, + "The FileTypeBox should contain 'mif1' in the compatible_brands list \ + per MIAF (ISO 23000-22:2019) § 7.2.1.2", + )?; + } + } else { + return Err(Error::InvalidData("'ftyp' box must occur first")); + } + } + + let mut meta = None; + let mut item_storage = TryVec::new(); + + while let Some(mut b) = iter.next_box()? { + trace!("read_avif parsing {:?} box", b.head.name); + match b.head.name { + BoxType::MetadataBox => { + if meta.is_some() { + return Err(Error::InvalidData( + "There should be zero or one meta boxes per ISOBMFF (ISO 14496-12:2020) § 8.11.1.1", + )); + } + meta = Some(read_avif_meta(&mut b, strictness)?); + } + BoxType::MediaDataBox => { + if b.bytes_left() > 0 { + let file_offset = b.offset(); + let data = b.read_into_try_vec()?; + item_storage.push(MediaDataBox { file_offset, data })?; + } + } + _ => skip_box_content(&mut b)?, + } + + check_parser_state!(b.content); + } + + let AvifMeta { + item_references, + item_properties, + primary_item_id, + iloc_items, + } = meta.ok_or(Error::InvalidData("missing meta"))?; + + let mut alpha_item_ids = item_references + .iter() + // Auxiliary image for the primary image + .filter(|iref| { + iref.to_item_id == primary_item_id + && iref.from_item_id != primary_item_id + && iref.item_type == b"auxl" + }) + .map(|iref| iref.from_item_id) + // which has the alpha property + .filter(|&item_id| item_properties.is_alpha(item_id)); + let alpha_item_id = alpha_item_ids.next(); + if alpha_item_ids.next().is_some() { + return Err(Error::InvalidData("multiple alpha planes")); + } + + let premultiplied_alpha = alpha_item_id.map_or(false, |alpha_item_id| { + item_references.iter().any(|iref| { + iref.from_item_id == primary_item_id + && iref.to_item_id == alpha_item_id + && iref.item_type == b"prem" + }) + }); + + let mut primary_item = None; + let mut alpha_item = None; + + // store data or record location of relevant items + for (item_id, loc) in iloc_items { + let item = if item_id == primary_item_id { + &mut primary_item + } else if Some(item_id) == alpha_item_id { + &mut alpha_item + } else { + continue; + }; + + if loc.construction_method != ConstructionMethod::File { + return Err(Error::Unsupported("unsupported construction_method")); + } + + assert!(item.is_none()); + + // If our item is spread over multiple extents, we'll need to copy it + // into a contiguous buffer. Otherwise, we can just store the extent + // and return a pointer into the mdat later to avoid the copy. + if loc.extents.len() > 1 { + *item = Some(AvifItem::with_inline_data(item_id)) + } + + for extent in loc.extents { + let mut found = false; + // try to find an mdat which contains the extent + for mdat in item_storage.iter_mut() { + if let Some(extent_slice) = mdat.get(&extent) { + match item { + None => { + trace!("Using IsobmffItem::Location"); + *item = Some(AvifItem::with_data_location(item_id, extent)); + } + Some(AvifItem { + image_data: IsobmffItem::Data(item_data), + .. + }) => { + trace!("Using IsobmffItem::Data"); + // We could potentially optimize memory usage by trying to avoid reading + // or storing mdat boxes which aren't used by our API, but for now it seems + // like unnecessary complexity + item_data.extend_from_slice(extent_slice)?; + } + _ => unreachable!(), + } + found = true; + break; + } + } + + if !found { + return Err(Error::InvalidData( + "iloc contains an extent that is not in any mdat", + )); + } + } + + assert!(item.is_some()); + } + + let primary_item = primary_item.ok_or(Error::InvalidData( + "Missing 'pitm' box, required per HEIF (ISO/IEC 23008-12:2017) § 10.2.1", + ))?; + + let has_pixi = |item_id| { + item_properties + .get(item_id, BoxType::PixelInformationBox) + .map_or(false, |opt| opt.is_some()) + }; + if !has_pixi(primary_item_id) || !alpha_item_id.map_or(true, has_pixi) { + // The requirement to include pixi is in the process of being changed + // to allowing its omission to imply a default value. In anticipation + // of that, only give an error in strict mode + // See https://github.com/MPEGGroup/MIAF/issues/9 + fail_if( + if cfg!(feature = "missing-pixi-permitted") { + strictness == ParseStrictness::Strict + } else { + strictness != ParseStrictness::Permissive + }, + "The pixel information property shall be associated with every image \ + that is displayable (not hidden) \ + per MIAF (ISO/IEC 23000-22:2019) specification § 7.3.6.6", + )?; + } + + let has_av1c = |item_id| { + item_properties + .get(item_id, BoxType::AV1CodecConfigurationBox) + .map_or(false, |opt| opt.is_some()) + }; + if !has_av1c(primary_item_id) || !alpha_item_id.map_or(true, has_av1c) { + fail_if( + strictness != ParseStrictness::Permissive, + "One AV1 Item Configuration Property (av1C) is mandatory for an \ + image item of type 'av01' \ + per AVIF specification § 2.2.1", + )?; + } + + if item_properties.get_ispe(primary_item_id)?.is_none() { + fail_if( + strictness != ParseStrictness::Permissive, + "Missing 'ispe' property for primary item, required \ + per HEIF (ISO/IEC 23008-12:2017) § 6.5.3.1", + )?; + } + + Ok(AvifContext { + strictness, + item_storage, + primary_item, + alpha_item, + premultiplied_alpha, + item_properties, + }) +} + +/// Parse a metadata box in the context of an AVIF +/// Currently requires the primary item to be an av01 item type and generates +/// an error otherwise. +/// See ISOBMFF (ISO 14496-12:2020) § 8.11.1 +fn read_avif_meta( + src: &mut BMFFBox, + strictness: ParseStrictness, +) -> Result { + let version = read_fullbox_version_no_flags(src)?; + + if version != 0 { + return Err(Error::Unsupported("unsupported meta version")); + } + + let mut read_handler_box = false; + let mut primary_item_id = None; + let mut item_infos = None; + let mut iloc_items = None; + let mut item_references = None; + let mut item_properties = None; + + let mut iter = src.box_iter(); + while let Some(mut b) = iter.next_box()? { + trace!("read_avif_meta parsing {:?} box", b.head.name); + + if !read_handler_box && b.head.name != BoxType::HandlerBox { + fail_if( + strictness != ParseStrictness::Permissive, + "The HandlerBox shall be the first contained box within the MetaBox \ + per MIAF (ISO 23000-22:2019) § 7.2.1.5", + )?; + } + + match b.head.name { + BoxType::HandlerBox => { + if read_handler_box { + return Err(Error::InvalidData( + "There shall be exactly one hdlr box per ISOBMFF (ISO 14496-12:2020) § 8.4.3.1", + )); + } + let HandlerBox { handler_type } = read_hdlr(&mut b, strictness)?; + if handler_type != b"pict" { + fail_if( + strictness != ParseStrictness::Permissive, + "The HandlerBox handler_type must be 'pict' \ + per MIAF (ISO 23000-22:2019) § 7.2.1.5", + )?; + } + read_handler_box = true; + } + BoxType::ItemInfoBox => { + if item_infos.is_some() { + return Err(Error::InvalidData( + "There shall be zero or one iinf boxes per ISOBMFF (ISO 14496-12:2020) § 8.11.6.1", + )); + } + item_infos = Some(read_iinf(&mut b, strictness)?); + } + BoxType::ItemLocationBox => { + if iloc_items.is_some() { + return Err(Error::InvalidData( + "There shall be zero or one iloc boxes per ISOBMFF (ISO 14496-12:2020) § 8.11.3.1", + )); + } + iloc_items = Some(read_iloc(&mut b)?); + } + BoxType::PrimaryItemBox => { + if primary_item_id.is_some() { + return Err(Error::InvalidData( + "There shall be zero or one pitm boxes per ISOBMFF (ISO 14496-12:2020) § 8.11.4.1", + )); + } + primary_item_id = Some(read_pitm(&mut b)?); + } + BoxType::ItemReferenceBox => { + if item_references.is_some() { + return Err(Error::InvalidData("There shall be zero or one iref boxes per ISOBMFF (ISO 14496-12:2020) § 8.11.12.1")); + } + item_references = Some(read_iref(&mut b)?); + } + BoxType::ItemPropertiesBox => { + if item_properties.is_some() { + return Err(Error::InvalidData("There shall be zero or one iprp boxes per ISOBMFF (ISO 14496-12:2020) § 8.11.14.1")); + } + item_properties = Some(read_iprp(&mut b, MIF1_BRAND, strictness)?); + } + _ => skip_box_content(&mut b)?, + } + + check_parser_state!(b.content); + } + + let primary_item_id = primary_item_id.ok_or(Error::InvalidData( + "Required pitm box not present in meta box", + ))?; + + let item_infos = item_infos.ok_or(Error::InvalidData("iinf missing"))?; + + if let Some(item_info) = item_infos.iter().find(|x| x.item_id == primary_item_id) { + debug!("primary_item_id type: {}", U32BE(item_info.item_type)); + match &item_info.item_type.to_be_bytes() { + b"av01" => {} + b"grid" => return Err(Error::from(Status::UnsupportedGrid)), + _ => { + return Err(Error::InvalidData( + "primary_item_id type is neither 'av01' nor 'grid'", + )) + } + } + } else { + return Err(Error::InvalidData( + "primary_item_id not present in iinf box", + )); + } + + Ok(AvifMeta { + item_properties: item_properties.unwrap_or_default(), + item_references: item_references.unwrap_or_default(), + primary_item_id, + iloc_items: iloc_items.ok_or(Error::InvalidData("iloc missing"))?, + }) +} + +/// Parse a Primary Item Box +/// See ISOBMFF (ISO 14496-12:2020) § 8.11.4 +fn read_pitm(src: &mut BMFFBox) -> Result { + let version = read_fullbox_version_no_flags(src)?; + + let item_id = ItemId(match version { + 0 => be_u16(src)?.into(), + 1 => be_u32(src)?, + _ => return Err(Error::Unsupported("unsupported pitm version")), + }); + + Ok(item_id) +} + +/// Parse an Item Information Box +/// See ISOBMFF (ISO 14496-12:2020) § 8.11.6 +fn read_iinf( + src: &mut BMFFBox, + strictness: ParseStrictness, +) -> Result> { + let version = read_fullbox_version_no_flags(src)?; + + match version { + 0 | 1 => (), + _ => return Err(Error::Unsupported("unsupported iinf version")), + } + + let entry_count = if version == 0 { + be_u16(src)?.to_usize() + } else { + be_u32(src)?.to_usize() + }; + let mut item_infos = TryVec::with_capacity(entry_count)?; + + let mut iter = src.box_iter(); + while let Some(mut b) = iter.next_box()? { + if b.head.name != BoxType::ItemInfoEntry { + return Err(Error::InvalidData( + "iinf box shall contain only infe boxes per ISOBMFF (ISO 14496-12:2020) § 8.11.6.2", + )); + } + + item_infos.push(read_infe(&mut b, strictness)?)?; + + check_parser_state!(b.content); + } + + Ok(item_infos) +} + +/// A simple wrapper to interpret a u32 as a 4-byte string in big-endian +/// order without requiring any allocation. +struct U32BE(u32); + +impl std::fmt::Display for U32BE { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match std::str::from_utf8(&self.0.to_be_bytes()) { + Ok(s) => f.write_str(s), + Err(_) => write!(f, "{:x?}", self.0), + } + } +} + +/// Parse an Item Info Entry +/// See ISOBMFF (ISO 14496-12:2020) § 8.11.6.2 +fn read_infe(src: &mut BMFFBox, strictness: ParseStrictness) -> Result { + let (version, flags) = read_fullbox_extra(src)?; + + // According to the standard, it seems the flags field shall be 0, but at + // least one sample AVIF image has a nonzero value. + // See https://github.com/AOMediaCodec/av1-avif/issues/146 + if flags != 0 { + fail_if( + strictness == ParseStrictness::Strict, + "'infe' flags field shall be 0 \ + per ISOBMFF (ISO 14496-12:2020) § 8.11.6.2", + )?; + } + + // mif1 brand (see HEIF (ISO 23008-12:2017) § 10.2.1) only requires v2 and 3 + let item_id = ItemId(match version { + 2 => be_u16(src)?.into(), + 3 => be_u32(src)?, + _ => return Err(Error::Unsupported("unsupported version in 'infe' box")), + }); + + let item_protection_index = be_u16(src)?; + + if item_protection_index != 0 { + return Err(Error::from(Status::UnsupportedIpro)); + } + + let item_type = be_u32(src)?; + debug!("infe {:?} item_type: {}", item_id, U32BE(item_type)); + + // There are some additional fields here, but they're not of interest to us + skip_box_remain(src)?; + + Ok(ItemInfoEntry { item_id, item_type }) +} + +/// Parse an Item Reference Box +/// See ISOBMFF (ISO 14496-12:2020) § 8.11.12 +fn read_iref(src: &mut BMFFBox) -> Result> { + let mut item_references = TryVec::new(); + let version = read_fullbox_version_no_flags(src)?; + if version > 1 { + return Err(Error::Unsupported("iref version")); + } + + let mut iter = src.box_iter(); + while let Some(mut b) = iter.next_box()? { + trace!("read_iref parsing {:?} referenceType", b.head.name); + let from_item_id = ItemId::read(&mut b, version)?; + let reference_count = be_u16(&mut b)?; + item_references.reserve(reference_count.to_usize())?; + for _ in 0..reference_count { + let to_item_id = ItemId::read(&mut b, version)?; + if from_item_id == to_item_id { + return Err(Error::InvalidData( + "from_item_id and to_item_id must be different", + )); + } + item_references.push(SingleItemTypeReferenceBox { + item_type: b.head.name.into(), + from_item_id, + to_item_id, + })?; + } + check_parser_state!(b.content); + } + Ok(item_references) +} + +/// Parse an Item Properties Box +/// +/// See ISOBMFF (ISO 14496-12:2020 § 8.11.14) +/// +/// Note: HEIF (ISO 23008-12:2017) § 9.3.1 also defines the `iprp` box and +/// related types, but lacks additional requirements specified in 14496-12:2020. +/// +/// Note: Currently HEIF (ISO 23008-12:2017) § 6.5.5.1 specifies "At most one" +/// `colr` box per item, but this is being amended in [DIS 23008-12](https://www.iso.org/standard/83650.html). +/// The new text is likely to be "At most one for a given value of `colour_type`", +/// so this implementation adheres to that language for forward compatibility. +fn read_iprp( + src: &mut BMFFBox, + brand: FourCC, + strictness: ParseStrictness, +) -> Result { + let mut iter = src.box_iter(); + + let properties = match iter.next_box()? { + Some(mut b) if b.head.name == BoxType::ItemPropertyContainerBox => { + read_ipco(&mut b, strictness) + } + Some(_) => Err(Error::InvalidData("unexpected iprp child")), + None => Err(Error::UnexpectedEOF), + }?; + + let mut ipma_version_and_flag_values_seen = TryVec::with_capacity(1)?; + let mut association_entries = TryVec::::new(); + + while let Some(mut b) = iter.next_box()? { + if b.head.name != BoxType::ItemPropertyAssociationBox { + return Err(Error::InvalidData("unexpected iprp child")); + } + + let (version, flags) = read_fullbox_extra(&mut b)?; + if ipma_version_and_flag_values_seen.contains(&(version, flags)) { + fail_if( + strictness != ParseStrictness::Permissive, + "There shall be at most one ItemPropertyAssociationbox with a given pair of \ + values of version and flags \ + per ISOBMFF (ISO 14496-12:2020 § 8.11.14.1", + )?; + } + if flags != 0 && properties.len() <= 127 { + fail_if( + strictness == ParseStrictness::Strict, + "Unless there are more than 127 properties in the ItemPropertyContainerBox, \ + flags should be equal to 0 \ + per ISOBMFF (ISO 14496-12:2020 § 8.11.14.1", + )?; + } + ipma_version_and_flag_values_seen.push((version, flags))?; + for association_entry in read_ipma(&mut b, strictness, version, flags)? { + if let Some(previous_entry) = association_entries + .iter() + .find(|e| association_entry.item_id == e.item_id) + { + error!( + "Duplicate ipma entries for item_id\n1: {:?}\n2: {:?}", + previous_entry, association_entry + ); + // It's technically possible to make sense of this situation by merging ipma + // boxes, but this is a "shall" requirement, so we'd only do it in + // ParseStrictness::Permissive mode, and this hasn't shown up in the wild + return Err(Error::InvalidData( + "There shall be at most one occurrence of a given item_ID, \ + in the set of ItemPropertyAssociationBox boxes \ + per ISOBMFF (ISO 14496-12:2020) § 8.11.14.1", + )); + } + + const TRANSFORM_ORDER_ERROR: &str = + "These properties, if used, shall be indicated to be applied \ + in the following order: clean aperture first, then rotation, \ + then mirror. \ + per MIAF (ISO/IEC 23000-22:2019) § 7.3.6.7"; + const TRANSFORM_ORDER: &[BoxType] = &[ + BoxType::CleanApertureBox, + BoxType::ImageRotation, + BoxType::ImageMirror, + ]; + let mut prev_transform_index = None; + // Realistically, there should only ever be 1 nclx and 1 icc + let mut colour_type_indexes: TryHashMap = + TryHashMap::with_capacity(2)?; + + for a in &association_entry.associations { + if a.property_index == PropertyIndex(0) { + if a.essential { + fail_if( + strictness != ParseStrictness::Permissive, + "the essential indicator shall be 0 for property index 0 \ + per ISOBMFF (ISO 14496-12:2020 § 8.11.14.3", + )?; + } + continue; + } + + if let Some(property) = properties.get(&a.property_index) { + assert!(brand == MIF1_BRAND); + match property { + ItemProperty::AV1Config(_) + | ItemProperty::Mirroring(_) + | ItemProperty::Rotation(_) => { + if !a.essential { + warn!("{:?} is invalid", property); + // This is a "shall", but it is likely to change, so only + // fail if using strict parsing. + // See https://github.com/mozilla/mp4parse-rust/issues/284 + fail_if( + strictness == ParseStrictness::Strict, + "All transformative properties associated with coded and \ + derived images required or conditionally required by this \ + document shall be marked as essential \ + per MIAF (ISO 23000-22:2019) § 7.3.9", + )?; + } + } + // XXX this is contrary to the published specification; see doc comment + // at the beginning of this function for more details + ItemProperty::Colour(colr) => { + let colour_type = colr.colour_type(); + if let Some(prev_colr_index) = colour_type_indexes.get(&colour_type) { + warn!( + "Multiple '{}' type colr associations with {:?}: {:?} and {:?}", + colour_type, + association_entry.item_id, + a.property_index, + prev_colr_index + ); + fail_if( + strictness != ParseStrictness::Permissive, + "Each item shall have at most one property association with a + ColourInformationBox (colr) for a given value of colour_type \ + per HEIF (ISO/IEC DIS 23008-12) § 6.5.5.1", + )?; + } else { + colour_type_indexes.insert(colour_type, a.property_index)?; + } + } + _ => {} + } + + if let Some(transform_index) = TRANSFORM_ORDER + .iter() + .position(|t| *t == property.box_type()) + { + if let Some(prev) = prev_transform_index { + if prev >= transform_index { + error!( + "{:?} after {:?}", + TRANSFORM_ORDER[transform_index], TRANSFORM_ORDER[prev] + ); + return Err(Error::InvalidData(TRANSFORM_ORDER_ERROR)); + } + } + prev_transform_index = Some(transform_index); + } + } else { + error!( + "Missing property at {:?} for {:?}", + a.property_index, association_entry.item_id + ); + fail_if( + strictness != ParseStrictness::Permissive, + "Invalid property index in ipma", + )?; + } + } + association_entries.push(association_entry)? + } + + check_parser_state!(b.content); + } + + let iprp = ItemPropertiesBox { + properties, + association_entries, + }; + trace!("read_iprp -> {:#?}", iprp); + Ok(iprp) +} + +/// See ISOBMFF (ISO 14496-12:2020 § 8.11.14.1 +#[derive(Debug)] +pub enum ItemProperty { + AuxiliaryType(AuxiliaryTypeProperty), + AV1Config(AV1ConfigBox), + Channels(PixelInformation), + Colour(ColourInformation), + ImageSpatialExtents(ImageSpatialExtentsProperty), + Mirroring(ImageMirror), + Rotation(ImageRotation), + /// Necessary to validate property indices in read_iprp + Unsupported(BoxType), +} + +impl ItemProperty { + fn box_type(&self) -> BoxType { + match self { + ItemProperty::AuxiliaryType(_) => BoxType::AuxiliaryTypeProperty, + ItemProperty::AV1Config(_) => BoxType::AV1CodecConfigurationBox, + ItemProperty::Colour(_) => BoxType::ColourInformationBox, + ItemProperty::Mirroring(_) => BoxType::ImageMirror, + ItemProperty::Rotation(_) => BoxType::ImageRotation, + ItemProperty::ImageSpatialExtents(_) => BoxType::ImageSpatialExtentsProperty, + ItemProperty::Channels(_) => BoxType::PixelInformationBox, + ItemProperty::Unsupported(box_type) => *box_type, + } + } +} + +#[derive(Debug)] +struct ItemPropertyAssociationEntry { + item_id: ItemId, + associations: TryVec, +} + +/// For storing ItemPropertyAssociation data +/// See ISOBMFF (ISO 14496-12:2020 § 8.11.14.1 +#[derive(Debug)] +struct Association { + essential: bool, + property_index: PropertyIndex, +} + +/// See ISOBMFF (ISO 14496-12:2020 § 8.11.14.1 +/// +/// The properties themselves are stored in `properties`, but the items they're +/// associated with are stored in `association_entries`. It's necessary to +/// maintain this indirection because multiple items can reference the same +/// property. For example, both the primary item and alpha item can share the +/// same [`ImageSpatialExtentsProperty`]. +#[derive(Debug, Default)] +pub struct ItemPropertiesBox { + /// `ItemPropertyContainerBox property_container` in the spec + properties: TryHashMap, + /// `ItemPropertyAssociationBox association[]` in the spec + association_entries: TryVec, +} + +impl ItemPropertiesBox { + /// For displayable images `av1C`, `pixi` and `ispe` are mandatory, `colr` + /// is typically included too, so we might as well use an even power of 2. + const MIN_PROPERTIES: usize = 4; + + fn is_alpha(&self, item_id: ItemId) -> bool { + match self.get(item_id, BoxType::AuxiliaryTypeProperty) { + Ok(Some(ItemProperty::AuxiliaryType(urn))) => { + urn.aux_type.as_slice() == "urn:mpeg:mpegB:cicp:systems:auxiliary:alpha".as_bytes() + } + Ok(Some(other_property)) => panic!("property key mismatch: {:?}", other_property), + Ok(None) => false, + Err(e) => { + error!( + "is_alpha: Error checking AuxiliaryTypeProperty ({}), returning false", + e + ); + false + } + } + } + + fn get_ispe(&self, item_id: ItemId) -> Result> { + if let Some(ItemProperty::ImageSpatialExtents(ispe)) = + self.get(item_id, BoxType::ImageSpatialExtentsProperty)? + { + Ok(Some(ispe)) + } else { + Ok(None) + } + } + + fn get(&self, item_id: ItemId, property_type: BoxType) -> Result> { + match self + .get_multiple(item_id, |prop| prop.box_type() == property_type)? + .as_slice() + { + &[] => Ok(None), + &[single_value] => Ok(Some(single_value)), + multiple_values => { + error!( + "Multiple values for {:?}: {:?}", + property_type, multiple_values + ); + // TODO: add test + Err(Error::InvalidData("conflicting item property values")) + } + } + } + + fn get_multiple( + &self, + item_id: ItemId, + filter: impl Fn(&ItemProperty) -> bool, + ) -> Result> { + let mut values = TryVec::new(); + for entry in &self.association_entries { + for a in &entry.associations { + if entry.item_id == item_id { + match self.properties.get(&a.property_index) { + Some(ItemProperty::Unsupported(_)) => {} + Some(property) if filter(property) => values.push(property)?, + _ => {} + } + } + } + } + + Ok(values) + } +} + +/// An upper bound which can be used to check overflow at compile time +trait UpperBounded { + const MAX: u64; +} + +/// Implement type $name as a newtype wrapper around an unsigned int which +/// implements the UpperBounded trait. +macro_rules! impl_bounded { + ( $name:ident, $inner:ty ) => { + #[derive(Clone, Copy)] + pub struct $name($inner); + + impl $name { + pub const fn new(n: $inner) -> Self { + Self(n) + } + + #[allow(dead_code)] + pub fn get(self) -> $inner { + self.0 + } + } + + impl UpperBounded for $name { + const MAX: u64 = <$inner>::MAX as u64; + } + }; +} + +/// Implement type $name as a type representing the product of two unsigned ints +/// which implements the UpperBounded trait. +macro_rules! impl_bounded_product { + ( $name:ident, $multiplier:ty, $multiplicand:ty, $inner:ty) => { + #[derive(Clone, Copy)] + pub struct $name($inner); + + impl $name { + pub fn new(value: $inner) -> Self { + assert!(value <= Self::MAX); + Self(value) + } + + pub fn get(self) -> $inner { + self.0 + } + } + + impl UpperBounded for $name { + const MAX: u64 = <$multiplier>::MAX * <$multiplicand>::MAX; + } + }; +} + +mod bounded_uints { + use UpperBounded; + + impl_bounded!(U8, u8); + impl_bounded!(U16, u16); + impl_bounded!(U32, u32); + impl_bounded!(U64, u64); + + impl_bounded_product!(U32MulU8, U32, U8, u64); + impl_bounded_product!(U32MulU16, U32, U16, u64); + + impl UpperBounded for std::num::NonZeroU8 { + const MAX: u64 = u8::MAX as u64; + } +} + +use bounded_uints::*; + +/// Implement the multiplication operator for $lhs * $rhs giving $output, which +/// is internally represented as $inner. The operation is statically checked +/// to ensure the product won't overflow $inner, nor exceed <$output>::MAX. +macro_rules! impl_mul { + ( ($lhs:ty , $rhs:ty) => ($output:ty, $inner:ty) ) => { + impl std::ops::Mul<$rhs> for $lhs { + type Output = $output; + + fn mul(self, rhs: $rhs) -> Self::Output { + static_assertions::const_assert!(<$output>::MAX <= <$inner>::MAX as u64); + static_assertions::const_assert!(<$lhs>::MAX * <$rhs>::MAX <= <$output>::MAX); + + let lhs: $inner = self.get().into(); + let rhs: $inner = rhs.get().into(); + Self::Output::new(lhs.checked_mul(rhs).expect("infallible")) + } + } + }; +} + +impl_mul!((U8, std::num::NonZeroU8) => (U16, u16)); +impl_mul!((U32, std::num::NonZeroU8) => (U32MulU8, u64)); +impl_mul!((U32, U16) => (U32MulU16, u64)); + +impl std::ops::Add for U32MulU8 { + type Output = U64; + + fn add(self, rhs: U32MulU16) -> Self::Output { + static_assertions::const_assert!(U32MulU8::MAX + U32MulU16::MAX < U64::MAX); + let lhs: u64 = self.get(); + let rhs: u64 = rhs.get(); + Self::Output::new(lhs.checked_add(rhs).expect("infallible")) + } +} + +const MAX_IPMA_ASSOCIATION_COUNT: U8 = U8::new(u8::MAX); + +/// After reading only the `entry_count` field of an ipma box, we can check its +/// basic validity and calculate (assuming validity) the number of associations +/// which will be contained (allowing preallocation of the storage). +/// All the arithmetic is compile-time verified to not overflow via supporting +/// types implementing the UpperBounded trait. Types are declared explicitly to +/// show there isn't any accidental inference to primitive types. +/// +/// See ISOBMFF (ISO 14496-12:2020 § 8.11.14.1 +fn calculate_ipma_total_associations( + version: u8, + bytes_left: u64, + entry_count: U32, + num_association_bytes: std::num::NonZeroU8, +) -> Result { + let min_entry_bytes = + std::num::NonZeroU8::new(1 /* association_count */ + if version == 0 { 2 } else { 4 }) + .unwrap(); + + let total_non_association_bytes: U32MulU8 = entry_count * min_entry_bytes; + let total_association_bytes: u64; + + if let Some(difference) = bytes_left.checked_sub(total_non_association_bytes.get()) { + // All the storage for the `essential` and `property_index` parts (assuming a valid ipma box size) + total_association_bytes = difference; + } else { + return Err(Error::InvalidData( + "ipma box below minimum size for entry_count", + )); + } + + let max_association_bytes_per_entry: U16 = MAX_IPMA_ASSOCIATION_COUNT * num_association_bytes; + let max_total_association_bytes: U32MulU16 = entry_count * max_association_bytes_per_entry; + let max_bytes_left: U64 = total_non_association_bytes + max_total_association_bytes; + + if bytes_left > max_bytes_left.get() { + return Err(Error::InvalidData( + "ipma box exceeds maximum size for entry_count", + )); + } + + let total_associations: u64 = total_association_bytes / u64::from(num_association_bytes.get()); + + Ok(total_associations.try_into()?) +} + +/// Parse an ItemPropertyAssociation box +/// +/// See ISOBMFF (ISO 14496-12:2020 § 8.11.14.1 +fn read_ipma( + src: &mut BMFFBox, + strictness: ParseStrictness, + version: u8, + flags: u32, +) -> Result> { + let entry_count = be_u32(src)?; + let num_association_bytes = + std::num::NonZeroU8::new(if flags & 1 == 1 { 2 } else { 1 }).unwrap(); + + let total_associations = calculate_ipma_total_associations( + version, + src.bytes_left(), + U32::new(entry_count), + num_association_bytes, + )?; + // Assuming most items will have at least `MIN_PROPERTIES` and knowing the + // total number of item -> property associations (`total_associations`), + // we can provide a good estimate for how many elements we'll need in this + // vector, even though we don't know precisely how many items there will be + // properties for. + let mut entries = TryVec::::with_capacity( + total_associations / ItemPropertiesBox::MIN_PROPERTIES, + )?; + + for _ in 0..entry_count { + let item_id = ItemId::read(src, version)?; + + if let Some(previous_association) = entries.last() { + #[allow(clippy::comparison_chain)] + if previous_association.item_id > item_id { + return Err(Error::InvalidData( + "Each ItemPropertyAssociation box shall be ordered by increasing item_ID", + )); + } else if previous_association.item_id == item_id { + return Err(Error::InvalidData("There shall be at most one association box for each item_ID, in any ItemPropertyAssociation box")); + } + } + + let association_count = src.read_u8()?; + let mut associations = TryVec::with_capacity(association_count.to_usize())?; + for _ in 0..association_count { + let association = src + .take(num_association_bytes.get().into()) + .read_into_try_vec()?; + let mut association = BitReader::new(association.as_slice()); + let essential = association.read_bool()?; + let property_index = + PropertyIndex(association.read_u16(association.remaining().try_into()?)?); + associations.push(Association { + essential, + property_index, + })?; + } + + entries.push(ItemPropertyAssociationEntry { + item_id, + associations, + })?; + } + + check_parser_state!(src.content); + + if version != 0 { + if let Some(ItemPropertyAssociationEntry { + item_id: max_item_id, + .. + }) = entries.last() + { + if *max_item_id <= ItemId(u16::MAX.into()) { + fail_if( + strictness == ParseStrictness::Strict, + "The ipma version 0 should be used unless 32-bit item_ID values are needed \ + per ISOBMFF (ISO 14496-12:2020 § 8.11.14.1", + )?; + } + } + } + + trace!("read_ipma -> {:#?}", entries); + + Ok(entries) +} + +/// Parse an ItemPropertyContainerBox +/// +/// For unsupported properties that we know about, return specific +/// [`Status`] UnsupportedXXXX variants. Unless running in +/// [`ParseStrictness::Permissive`] mode, in which case, unsupported properties +/// will be ignored. +/// +/// See ISOBMFF (ISO 14496-12:2020 § 8.11.14.1 +fn read_ipco( + src: &mut BMFFBox, + strictness: ParseStrictness, +) -> Result> { + let mut properties = TryHashMap::with_capacity(ItemPropertiesBox::MIN_PROPERTIES)?; + + let mut index = PropertyIndex(1); // ipma uses 1-based indexing + let mut iter = src.box_iter(); + while let Some(mut b) = iter.next_box()? { + if let Some(property) = match b.head.name { + BoxType::AuxiliaryTypeProperty => Some(ItemProperty::AuxiliaryType(read_auxc(&mut b)?)), + BoxType::AV1CodecConfigurationBox => Some(ItemProperty::AV1Config(read_av1c(&mut b)?)), + BoxType::AV1LayeredImageIndexingProperty + if strictness != ParseStrictness::Permissive => + { + return Err(Error::from(Status::UnsupportedA1lx)) + } + BoxType::CleanApertureBox if strictness != ParseStrictness::Permissive => { + return Err(Error::from(Status::UnsupportedClap)) + } + BoxType::ColourInformationBox => { + Some(ItemProperty::Colour(read_colr(&mut b, strictness)?)) + } + BoxType::ImageMirror => Some(ItemProperty::Mirroring(read_imir(&mut b)?)), + BoxType::ImageRotation => Some(ItemProperty::Rotation(read_irot(&mut b)?)), + BoxType::ImageSpatialExtentsProperty => { + Some(ItemProperty::ImageSpatialExtents(read_ispe(&mut b)?)) + } + BoxType::LayerSelectorProperty if strictness != ParseStrictness::Permissive => { + return Err(Error::from(Status::UnsupportedLsel)) + } + BoxType::OperatingPointSelectorProperty + if strictness != ParseStrictness::Permissive => + { + return Err(Error::from(Status::UnsupportedA1op)) + } + BoxType::PixelInformationBox => Some(ItemProperty::Channels(read_pixi(&mut b)?)), + other_box_type => { + // Though we don't do anything with other property types, we still store + // a record at the index to identify invalid indices in ipma boxes + skip_box_remain(&mut b)?; + let item_property = ItemProperty::Unsupported(other_box_type); + debug!("Storing empty record {:?}", item_property); + Some(item_property) + } + } { + properties.insert(index, property)?; + } + + index = PropertyIndex( + index + .0 + .checked_add(1) // must include ignored properties to have correct indexes + .ok_or(Error::InvalidData("ipco index overflow"))?, + ); + + check_parser_state!(b.content); + } + + Ok(properties) +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct ImageSpatialExtentsProperty { + image_width: u32, + image_height: u32, +} + +/// Parse image spatial extents property +/// +/// See HEIF (ISO 23008-12:2017) § 6.5.3.1 +fn read_ispe(src: &mut BMFFBox) -> Result { + if read_fullbox_version_no_flags(src)? != 0 { + return Err(Error::Unsupported("ispe version")); + } + + let image_width = be_u32(src)?; + let image_height = be_u32(src)?; + + Ok(ImageSpatialExtentsProperty { + image_width, + image_height, + }) +} + +#[derive(Debug)] +pub struct PixelInformation { + bits_per_channel: TryVec, +} + +/// Parse pixel information +/// See HEIF (ISO 23008-12:2017) § 6.5.6 +fn read_pixi(src: &mut BMFFBox) -> Result { + let version = read_fullbox_version_no_flags(src)?; + if version != 0 { + return Err(Error::Unsupported("pixi version")); + } + + let num_channels = src.read_u8()?; + let mut bits_per_channel = TryVec::with_capacity(num_channels.to_usize())?; + let num_channels_read = src.try_read_to_end(&mut bits_per_channel)?; + + if u8::try_from(num_channels_read)? != num_channels { + return Err(Error::InvalidData("invalid num_channels")); + } + + check_parser_state!(src.content); + Ok(PixelInformation { bits_per_channel }) +} + +/// Despite [Rec. ITU-T H.273] (12/2016) defining the CICP fields as having a +/// range of 0-255, and only a small fraction of those values being used, +/// ISOBMFF (ISO 14496-12:2020) § 12.1.5 defines them as 16-bit values in the +/// `colr` box. Since we have no use for the additional range, and it would +/// complicate matters later, we fallibly convert before storing the input. +/// +/// [Rec. ITU-T H.273]: https://www.itu.int/rec/T-REC-H.273-201612-I/en +#[repr(C)] +#[derive(Debug)] +pub struct NclxColourInformation { + colour_primaries: u8, + transfer_characteristics: u8, + matrix_coefficients: u8, + full_range_flag: bool, +} + +/// The raw bytes of the ICC profile +#[repr(C)] +pub struct IccColourInformation { + bytes: TryVec, +} + +impl fmt::Debug for IccColourInformation { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("IccColourInformation") + .field("data", &format_args!("{} bytes", self.bytes.len())) + .finish() + } +} + +#[repr(C)] +#[derive(Debug)] +pub enum ColourInformation { + Nclx(NclxColourInformation), + Icc(IccColourInformation, FourCC), +} + +impl ColourInformation { + fn colour_type(&self) -> FourCC { + match self { + Self::Nclx(_) => FourCC::from(*b"nclx"), + Self::Icc(_, colour_type) => colour_type.clone(), + } + } +} + +/// Parse colour information +/// See ISOBMFF (ISO 14496-12:2020) § 12.1.5 +fn read_colr( + src: &mut BMFFBox, + strictness: ParseStrictness, +) -> Result { + let colour_type = be_u32(src)?.to_be_bytes(); + + match &colour_type { + b"nclx" => { + const NUM_RESERVED_BITS: u8 = 7; + let colour_primaries = be_u16(src)?.try_into()?; + let transfer_characteristics = be_u16(src)?.try_into()?; + let matrix_coefficients = be_u16(src)?.try_into()?; + let bytes = src.read_into_try_vec()?; + let mut bit_reader = BitReader::new(&bytes); + let full_range_flag = bit_reader.read_bool()?; + if bit_reader.remaining() != NUM_RESERVED_BITS.into() { + error!( + "read_colr expected {} reserved bits, found {}", + NUM_RESERVED_BITS, + bit_reader.remaining() + ); + return Err(Error::InvalidData("Unexpected size for colr box")); + } + if bit_reader.read_u8(NUM_RESERVED_BITS)? != 0 { + fail_if( + strictness != ParseStrictness::Permissive, + "The 7 reserved bits at the end of the ColourInformationBox \ + for colour_type == 'nclx' must be 0 \ + per ISOBMFF (ISO 14496-12:2020) § 12.1.5.2", + )?; + } + + Ok(ColourInformation::Nclx(NclxColourInformation { + colour_primaries, + transfer_characteristics, + matrix_coefficients, + full_range_flag, + })) + } + b"rICC" | b"prof" => Ok(ColourInformation::Icc( + IccColourInformation { + bytes: src.read_into_try_vec()?, + }, + FourCC::from(colour_type), + )), + _ => { + error!("read_colr colour_type: {:?}", colour_type); + Err(Error::InvalidData( + "Unsupported colour_type for ColourInformationBox", + )) + } + } +} + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +/// Rotation in the positive (that is, anticlockwise) direction +/// Visualized in terms of starting with (⥠) UPWARDS HARPOON WITH BARB LEFT FROM BAR +/// similar to a DIGIT ONE (1) +pub enum ImageRotation { + /// ⥠ UPWARDS HARPOON WITH BARB LEFT FROM BAR + D0, + /// ⥞ LEFTWARDS HARPOON WITH BARB DOWN FROM BAR + D90, + /// ⥝ DOWNWARDS HARPOON WITH BARB RIGHT FROM BAR + D180, + /// ⥛ RIGHTWARDS HARPOON WITH BARB UP FROM BAR + D270, +} + +/// Parse image rotation box +/// See HEIF (ISO 23008-12:2017) § 6.5.10 +fn read_irot(src: &mut BMFFBox) -> Result { + let irot = src.read_into_try_vec()?; + let mut irot = BitReader::new(&irot); + let _reserved = irot.read_u8(6)?; + let image_rotation = match irot.read_u8(2)? { + 0 => ImageRotation::D0, + 1 => ImageRotation::D90, + 2 => ImageRotation::D180, + 3 => ImageRotation::D270, + _ => unreachable!(), + }; + + check_parser_state!(src.content); + + Ok(image_rotation) +} + +/// The axis about which the image is mirrored (opposite of flip) +/// Visualized in terms of starting with (⥠) UPWARDS HARPOON WITH BARB LEFT FROM BAR +/// similar to a DIGIT ONE (1) +#[repr(C)] +#[derive(Debug)] +pub enum ImageMirror { + /// top and bottom parts exchanged + /// ⥡ DOWNWARDS HARPOON WITH BARB LEFT FROM BAR + TopBottom, + /// left and right parts exchanged + /// ⥜ UPWARDS HARPOON WITH BARB RIGHT FROM BAR + LeftRight, +} + +/// Parse image mirroring box +/// See HEIF (ISO 23008-12:2017) § 6.5.12
+/// Note: [ISO/IEC 23008-12:2017/DAmd 2](https://www.iso.org/standard/81688.html) +/// reverses the interpretation of the 'imir' box in § 6.5.12.3: +/// > `axis` specifies a vertical (`axis` = 0) or horizontal (`axis` = 1) axis +/// > for the mirroring operation. +/// +/// is replaced with: +/// > `mode` specifies how the mirroring is performed: 0 indicates that the top +/// > and bottom parts of the image are exchanged; 1 specifies that the left and +/// > right parts are exchanged. +/// > +/// > NOTE: In Exif, orientation tag can be used to signal mirroring operations. +/// > Exif orientation tag 4 corresponds to `mode` = 0 of `ImageMirror`, and +/// > Exif orientation tag 2 corresponds to `mode` = 1 accordingly. +/// +/// This implementation conforms to the text in Draft Amendment 2, which is the +/// opposite of the published standard as of 4 June 2021. +fn read_imir(src: &mut BMFFBox) -> Result { + let imir = src.read_into_try_vec()?; + let mut imir = BitReader::new(&imir); + let _reserved = imir.read_u8(7)?; + let image_mirror = match imir.read_u8(1)? { + 0 => ImageMirror::TopBottom, + 1 => ImageMirror::LeftRight, + _ => unreachable!(), + }; + + check_parser_state!(src.content); + + Ok(image_mirror) +} + +/// See HEIF (ISO 23008-12:2017) § 6.5.8 +#[derive(Debug, PartialEq)] +pub struct AuxiliaryTypeProperty { + aux_type: TryString, + aux_subtype: TryString, +} + +/// Parse image properties for auxiliary images +/// See HEIF (ISO 23008-12:2017) § 6.5.8 +fn read_auxc(src: &mut BMFFBox) -> Result { + let version = read_fullbox_version_no_flags(src)?; + if version != 0 { + return Err(Error::Unsupported("auxC version")); + } + + let mut aux = TryString::new(); + src.try_read_to_end(&mut aux)?; + + let (aux_type, aux_subtype): (TryString, TryVec); + if let Some(nul_byte_pos) = aux.iter().position(|&b| b == b'\0') { + let (a, b) = aux.as_slice().split_at(nul_byte_pos); + aux_type = a.try_into()?; + aux_subtype = (&b[1..]).try_into()?; + } else { + aux_type = aux; + aux_subtype = TryVec::new(); + } + + Ok(AuxiliaryTypeProperty { + aux_type, + aux_subtype, + }) +} + +/// Parse an item location box inside a meta box +/// See ISOBMFF (ISO 14496-12:2020) § 8.11.3 +fn read_iloc(src: &mut BMFFBox) -> Result> { + let version: IlocVersion = read_fullbox_version_no_flags(src)?.try_into()?; + + let iloc = src.read_into_try_vec()?; + let mut iloc = BitReader::new(&iloc); + + let offset_size: IlocFieldSize = iloc.read_u8(4)?.try_into()?; + let length_size: IlocFieldSize = iloc.read_u8(4)?.try_into()?; + let base_offset_size: IlocFieldSize = iloc.read_u8(4)?.try_into()?; + + let index_size: Option = match version { + IlocVersion::One | IlocVersion::Two => Some(iloc.read_u8(4)?.try_into()?), + IlocVersion::Zero => { + let _reserved = iloc.read_u8(4)?; + None + } + }; + + let item_count = match version { + IlocVersion::Zero | IlocVersion::One => iloc.read_u32(16)?, + IlocVersion::Two => iloc.read_u32(32)?, + }; + + let mut items = TryHashMap::with_capacity(item_count.to_usize())?; + + for _ in 0..item_count { + let item_id = ItemId(match version { + IlocVersion::Zero | IlocVersion::One => iloc.read_u32(16)?, + IlocVersion::Two => iloc.read_u32(32)?, + }); + + // The spec isn't entirely clear how an `iloc` should be interpreted for version 0, + // which has no `construction_method` field. It does say: + // "For maximum compatibility, version 0 of this box should be used in preference to + // version 1 with `construction_method==0`, or version 2 when possible." + // We take this to imply version 0 can be interpreted as using file offsets. + let construction_method = match version { + IlocVersion::Zero => ConstructionMethod::File, + IlocVersion::One | IlocVersion::Two => { + let _reserved = iloc.read_u16(12)?; + match iloc.read_u16(4)? { + 0 => ConstructionMethod::File, + 1 => ConstructionMethod::Idat, + 2 => return Err(Error::Unsupported("construction_method 'item_offset' is not supported")), + _ => return Err(Error::InvalidData("construction_method is taken from the set 0, 1 or 2 per ISOBMFF (ISO 14496-12:2020) § 8.11.3.3")) + } + } + }; + + let data_reference_index = iloc.read_u16(16)?; + + if data_reference_index != 0 { + return Err(Error::Unsupported( + "external file references (iloc.data_reference_index != 0) are not supported", + )); + } + + let base_offset = iloc.read_u64(base_offset_size.as_bits())?; + let extent_count = iloc.read_u16(16)?; + + if extent_count < 1 { + return Err(Error::InvalidData( + "extent_count must have a value 1 or greater per ISOBMFF (ISO 14496-12:2020) § 8.11.3.3", + )); + } + + // "If only one extent is used (extent_count = 1) then either or both of the + // offset and length may be implied" + if extent_count != 1 + && (offset_size == IlocFieldSize::Zero || length_size == IlocFieldSize::Zero) + { + return Err(Error::InvalidData( + "extent_count != 1 requires explicit offset and length per ISOBMFF (ISO 14496-12:2020) § 8.11.3.3", + )); + } + + let mut extents = TryVec::with_capacity(extent_count.to_usize())?; + + for _ in 0..extent_count { + // Parsed but currently ignored, see `Extent` + let _extent_index = match &index_size { + None | Some(IlocFieldSize::Zero) => None, + Some(index_size) => { + debug_assert!(version == IlocVersion::One || version == IlocVersion::Two); + Some(iloc.read_u64(index_size.as_bits())?) + } + }; + + // Per ISOBMFF (ISO 14496-12:2020) § 8.11.3.1: + // "If the offset is not identified (the field has a length of zero), then the + // beginning of the source (offset 0) is implied" + // This behavior will follow from BitReader::read_u64(0) -> 0. + let extent_offset = iloc.read_u64(offset_size.as_bits())?; + let extent_length = iloc.read_u64(length_size.as_bits())?.try_into()?; + + // "If the length is not specified, or specified as zero, then the entire length of + // the source is implied" (ibid) + let offset = base_offset + .checked_add(extent_offset) + .ok_or(Error::InvalidData("offset calculation overflow"))?; + let extent = if extent_length == 0 { + Extent::ToEnd { offset } + } else { + Extent::WithLength { + offset, + len: extent_length, + } + }; + + extents.push(extent)?; + } + + let loc = ItemLocationBoxItem { + construction_method, + extents, + }; + + if items.insert(item_id, loc)?.is_some() { + return Err(Error::InvalidData("duplicate item_ID in iloc")); + } + } + + if iloc.remaining() == 0 { + Ok(items) + } else { + Err(Error::InvalidData("invalid iloc size")) + } +} + +/// Read the contents of a box, including sub boxes. +pub fn read_mp4(f: &mut T) -> Result { + let mut context = None; + let mut found_ftyp = false; + // TODO(kinetik): Top-level parsing should handle zero-sized boxes + // rather than throwing an error. + let mut iter = BoxIter::new(f); + while let Some(mut b) = iter.next_box()? { + // box ordering: ftyp before any variable length box (inc. moov), + // but may not be first box in file if file signatures etc. present + // fragmented mp4 order: ftyp, moov, pairs of moof/mdat (1-multiple), mfra + + // "special": uuid, wide (= 8 bytes) + // isom: moov, mdat, free, skip, udta, ftyp, moof, mfra + // iso2: pdin, meta + // iso3: meco + // iso5: styp, sidx, ssix, prft + // unknown, maybe: id32 + + // qt: pnot + + // possibly allow anything where all printable and/or all lowercase printable + // "four printable characters from the ISO 8859-1 character set" + match b.head.name { + BoxType::FileTypeBox => { + let ftyp = read_ftyp(&mut b)?; + found_ftyp = true; + debug!("{:?}", ftyp); + } + BoxType::MovieBox => { + context = Some(read_moov(&mut b, context)?); + } + #[cfg(feature = "meta-xml")] + BoxType::MetadataBox => { + if let Some(ctx) = &mut context { + ctx.metadata = Some(read_meta(&mut b)); + } + } + _ => skip_box_content(&mut b)?, + }; + check_parser_state!(b.content); + if context.is_some() { + debug!( + "found moov {}, could stop pure 'moov' parser now", + if found_ftyp { + "and ftyp" + } else { + "but no ftyp" + } + ); + } + } + + // XXX(kinetik): This isn't perfect, as a "moov" with no contents is + // treated as okay but we haven't found anything useful. Needs more + // thought for clearer behaviour here. + context.ok_or(Error::NoMoov) +} + +/// Parse a Movie Header Box +/// See ISOBMFF (ISO 14496-12:2020) § 8.2.2 +fn parse_mvhd(f: &mut BMFFBox) -> Result> { + let mvhd = read_mvhd(f)?; + debug!("{:?}", mvhd); + if mvhd.timescale == 0 { + return Err(Error::InvalidData("zero timescale in mdhd")); + } + let timescale = Some(MediaTimeScale(u64::from(mvhd.timescale))); + Ok(timescale) +} + +/// Parse a Movie Box +/// See ISOBMFF (ISO 14496-12:2020) § 8.2.1 +/// Note that despite the spec indicating "exactly one" moov box should exist at +/// the file container level, we support reading and merging multiple moov boxes +/// such as with tests/test_case_1185230.mp4. +fn read_moov(f: &mut BMFFBox, context: Option) -> Result { + let MediaContext { + mut timescale, + mut tracks, + mut mvex, + mut psshs, + mut userdata, + #[cfg(feature = "meta-xml")] + metadata, + } = context.unwrap_or_default(); + + let mut iter = f.box_iter(); + while let Some(mut b) = iter.next_box()? { + match b.head.name { + BoxType::MovieHeaderBox => { + timescale = parse_mvhd(&mut b)?; + } + BoxType::TrackBox => { + let mut track = Track::new(tracks.len()); + read_trak(&mut b, &mut track)?; + tracks.push(track)?; + } + BoxType::MovieExtendsBox => { + mvex = Some(read_mvex(&mut b)?); + debug!("{:?}", mvex); + } + BoxType::ProtectionSystemSpecificHeaderBox => { + let pssh = read_pssh(&mut b)?; + debug!("{:?}", pssh); + psshs.push(pssh)?; + } + BoxType::UserdataBox => { + userdata = Some(read_udta(&mut b)); + debug!("{:?}", userdata); + if let Some(Err(_)) = userdata { + // There was an error parsing userdata. Such failures are not fatal to overall + // parsing, just skip the rest of the box. + skip_box_remain(&mut b)?; + } + } + _ => skip_box_content(&mut b)?, + }; + check_parser_state!(b.content); + } + + Ok(MediaContext { + timescale, + tracks, + mvex, + psshs, + userdata, + #[cfg(feature = "meta-xml")] + metadata, + }) +} + +fn read_pssh(src: &mut BMFFBox) -> Result { + let len = src.bytes_left(); + let mut box_content = read_buf(src, len)?; + let (system_id, kid, data) = { + let pssh = &mut Cursor::new(&box_content); + + let (version, _) = read_fullbox_extra(pssh)?; + + let system_id = read_buf(pssh, 16)?; + + let mut kid = TryVec::::new(); + if version > 0 { + let count = be_u32(pssh)?; + for _ in 0..count { + let item = read_buf(pssh, 16)?; + kid.push(item)?; + } + } + + let data_size = be_u32(pssh)?; + let data = read_buf(pssh, data_size.into())?; + + (system_id, kid, data) + }; + + let mut pssh_box = TryVec::new(); + write_be_u32(&mut pssh_box, src.head.size.try_into()?)?; + pssh_box.extend_from_slice(b"pssh")?; + pssh_box.append(&mut box_content)?; + + Ok(ProtectionSystemSpecificHeaderBox { + system_id, + kid, + data, + box_content: pssh_box, + }) +} + +/// Parse a Movie Extends Box +/// See ISOBMFF (ISO 14496-12:2020) § 8.8.1 +fn read_mvex(src: &mut BMFFBox) -> Result { + let mut iter = src.box_iter(); + let mut fragment_duration = None; + while let Some(mut b) = iter.next_box()? { + match b.head.name { + BoxType::MovieExtendsHeaderBox => { + let duration = read_mehd(&mut b)?; + fragment_duration = Some(duration); + } + _ => skip_box_content(&mut b)?, + } + } + Ok(MovieExtendsBox { fragment_duration }) +} + +fn read_mehd(src: &mut BMFFBox) -> Result { + let (version, _) = read_fullbox_extra(src)?; + let fragment_duration = match version { + 1 => be_u64(src)?, + 0 => u64::from(be_u32(src)?), + _ => return Err(Error::InvalidData("unhandled mehd version")), + }; + Ok(MediaScaledTime(fragment_duration)) +} + +/// Parse a Track Box +/// See ISOBMFF (ISO 14496-12:2020) § 8.3.1. +fn read_trak(f: &mut BMFFBox, track: &mut Track) -> Result<()> { + let mut iter = f.box_iter(); + while let Some(mut b) = iter.next_box()? { + match b.head.name { + BoxType::TrackHeaderBox => { + let tkhd = read_tkhd(&mut b)?; + track.track_id = Some(tkhd.track_id); + track.tkhd = Some(tkhd.clone()); + debug!("{:?}", tkhd); + } + BoxType::EditBox => read_edts(&mut b, track)?, + BoxType::MediaBox => read_mdia(&mut b, track)?, + _ => skip_box_content(&mut b)?, + }; + check_parser_state!(b.content); + } + Ok(()) +} + +fn read_edts(f: &mut BMFFBox, track: &mut Track) -> Result<()> { + let mut iter = f.box_iter(); + while let Some(mut b) = iter.next_box()? { + match b.head.name { + BoxType::EditListBox => { + let elst = read_elst(&mut b)?; + if elst.edits.is_empty() { + debug!("empty edit list"); + continue; + } + let mut empty_duration = 0; + let mut idx = 0; + if elst.edits[idx].media_time == -1 { + if elst.edits.len() < 2 { + debug!("expected additional edit, ignoring edit list"); + continue; + } + empty_duration = elst.edits[idx].segment_duration; + idx += 1; + } + track.empty_duration = Some(MediaScaledTime(empty_duration)); + let media_time = elst.edits[idx].media_time; + if media_time < 0 { + debug!("unexpected negative media time in edit"); + } + track.media_time = Some(TrackScaledTime::( + std::cmp::max(0, media_time) as u64, + track.id, + )); + if elst.edits.len() > 2 { + debug!("ignoring edit list with {} entries", elst.edits.len()); + } + debug!("{:?}", elst); + } + _ => skip_box_content(&mut b)?, + }; + check_parser_state!(b.content); + } + Ok(()) +} + +#[allow(clippy::type_complexity)] // Allow the complex return, maybe rework in future +fn parse_mdhd( + f: &mut BMFFBox, + track: &mut Track, +) -> Result<( + MediaHeaderBox, + Option>, + Option>, +)> { + let mdhd = read_mdhd(f)?; + let duration = match mdhd.duration { + std::u64::MAX => None, + duration => Some(TrackScaledTime::(duration, track.id)), + }; + if mdhd.timescale == 0 { + return Err(Error::InvalidData("zero timescale in mdhd")); + } + let timescale = Some(TrackTimeScale::(u64::from(mdhd.timescale), track.id)); + Ok((mdhd, duration, timescale)) +} + +fn read_mdia(f: &mut BMFFBox, track: &mut Track) -> Result<()> { + let mut iter = f.box_iter(); + while let Some(mut b) = iter.next_box()? { + match b.head.name { + BoxType::MediaHeaderBox => { + let (mdhd, duration, timescale) = parse_mdhd(&mut b, track)?; + track.duration = duration; + track.timescale = timescale; + debug!("{:?}", mdhd); + } + BoxType::HandlerBox => { + let hdlr = read_hdlr(&mut b, ParseStrictness::Permissive)?; + + match hdlr.handler_type.value.as_ref() { + b"vide" => track.track_type = TrackType::Video, + b"soun" => track.track_type = TrackType::Audio, + b"meta" => track.track_type = TrackType::Metadata, + _ => (), + } + debug!("{:?}", hdlr); + } + BoxType::MediaInformationBox => read_minf(&mut b, track)?, + _ => skip_box_content(&mut b)?, + }; + check_parser_state!(b.content); + } + Ok(()) +} + +fn read_minf(f: &mut BMFFBox, track: &mut Track) -> Result<()> { + let mut iter = f.box_iter(); + while let Some(mut b) = iter.next_box()? { + match b.head.name { + BoxType::SampleTableBox => read_stbl(&mut b, track)?, + _ => skip_box_content(&mut b)?, + }; + check_parser_state!(b.content); + } + Ok(()) +} + +fn read_stbl(f: &mut BMFFBox, track: &mut Track) -> Result<()> { + let mut iter = f.box_iter(); + while let Some(mut b) = iter.next_box()? { + match b.head.name { + BoxType::SampleDescriptionBox => { + let stsd = read_stsd(&mut b, track)?; + debug!("{:?}", stsd); + track.stsd = Some(stsd); + } + BoxType::TimeToSampleBox => { + let stts = read_stts(&mut b)?; + debug!("{:?}", stts); + track.stts = Some(stts); + } + BoxType::SampleToChunkBox => { + let stsc = read_stsc(&mut b)?; + debug!("{:?}", stsc); + track.stsc = Some(stsc); + } + BoxType::SampleSizeBox => { + let stsz = read_stsz(&mut b)?; + debug!("{:?}", stsz); + track.stsz = Some(stsz); + } + BoxType::ChunkOffsetBox => { + let stco = read_stco(&mut b)?; + debug!("{:?}", stco); + track.stco = Some(stco); + } + BoxType::ChunkLargeOffsetBox => { + let co64 = read_co64(&mut b)?; + debug!("{:?}", co64); + track.stco = Some(co64); + } + BoxType::SyncSampleBox => { + let stss = read_stss(&mut b)?; + debug!("{:?}", stss); + track.stss = Some(stss); + } + BoxType::CompositionOffsetBox => { + let ctts = read_ctts(&mut b)?; + debug!("{:?}", ctts); + track.ctts = Some(ctts); + } + _ => skip_box_content(&mut b)?, + }; + check_parser_state!(b.content); + } + Ok(()) +} + +/// Parse an ftyp box. +/// See ISOBMFF (ISO 14496-12:2020) § 4.3 +fn read_ftyp(src: &mut BMFFBox) -> Result { + let major = be_u32(src)?; + let minor = be_u32(src)?; + let bytes_left = src.bytes_left(); + if bytes_left % 4 != 0 { + return Err(Error::InvalidData("invalid ftyp size")); + } + // Is a brand_count of zero valid? + let brand_count = bytes_left / 4; + let mut brands = TryVec::with_capacity(brand_count.try_into()?)?; + for _ in 0..brand_count { + brands.push(be_u32(src)?.into())?; + } + Ok(FileTypeBox { + major_brand: From::from(major), + minor_version: minor, + compatible_brands: brands, + }) +} + +/// Parse an mvhd box. +fn read_mvhd(src: &mut BMFFBox) -> Result { + let (version, _) = read_fullbox_extra(src)?; + match version { + // 64 bit creation and modification times. + 1 => { + skip(src, 16)?; + } + // 32 bit creation and modification times. + 0 => { + skip(src, 8)?; + } + _ => return Err(Error::InvalidData("unhandled mvhd version")), + } + let timescale = be_u32(src)?; + let duration = match version { + 1 => be_u64(src)?, + 0 => { + let d = be_u32(src)?; + if d == std::u32::MAX { + std::u64::MAX + } else { + u64::from(d) + } + } + _ => return Err(Error::InvalidData("unhandled mvhd version")), + }; + // Skip remaining fields. + skip(src, 80)?; + Ok(MovieHeaderBox { + timescale, + duration, + }) +} + +/// Parse a tkhd box. +fn read_tkhd(src: &mut BMFFBox) -> Result { + let (version, flags) = read_fullbox_extra(src)?; + let disabled = flags & 0x1u32 == 0 || flags & 0x2u32 == 0; + match version { + // 64 bit creation and modification times. + 1 => { + skip(src, 16)?; + } + // 32 bit creation and modification times. + 0 => { + skip(src, 8)?; + } + _ => return Err(Error::InvalidData("unhandled tkhd version")), + } + let track_id = be_u32(src)?; + skip(src, 4)?; + let duration = match version { + 1 => be_u64(src)?, + 0 => u64::from(be_u32(src)?), + _ => return Err(Error::InvalidData("unhandled tkhd version")), + }; + // Skip uninteresting fields. + skip(src, 16)?; + + let matrix = Matrix { + a: be_i32(src)?, + b: be_i32(src)?, + u: be_i32(src)?, + c: be_i32(src)?, + d: be_i32(src)?, + v: be_i32(src)?, + x: be_i32(src)?, + y: be_i32(src)?, + w: be_i32(src)?, + }; + + let width = be_u32(src)?; + let height = be_u32(src)?; + Ok(TrackHeaderBox { + track_id, + disabled, + duration, + width, + height, + matrix, + }) +} + +/// Parse a elst box. +/// See ISOBMFF (ISO 14496-12:2020) § 8.6.6 +fn read_elst(src: &mut BMFFBox) -> Result { + let (version, _) = read_fullbox_extra(src)?; + let edit_count = be_u32(src)?; + let mut edits = TryVec::with_capacity(edit_count.to_usize())?; + for _ in 0..edit_count { + let (segment_duration, media_time) = match version { + 1 => { + // 64 bit segment duration and media times. + (be_u64(src)?, be_i64(src)?) + } + 0 => { + // 32 bit segment duration and media times. + (u64::from(be_u32(src)?), i64::from(be_i32(src)?)) + } + _ => return Err(Error::InvalidData("unhandled elst version")), + }; + let media_rate_integer = be_i16(src)?; + let media_rate_fraction = be_i16(src)?; + edits.push(Edit { + segment_duration, + media_time, + media_rate_integer, + media_rate_fraction, + })?; + } + + // Padding could be added in some contents. + skip_box_remain(src)?; + + Ok(EditListBox { edits }) +} + +/// Parse a mdhd box. +fn read_mdhd(src: &mut BMFFBox) -> Result { + let (version, _) = read_fullbox_extra(src)?; + let (timescale, duration) = match version { + 1 => { + // Skip 64-bit creation and modification times. + skip(src, 16)?; + + // 64 bit duration. + (be_u32(src)?, be_u64(src)?) + } + 0 => { + // Skip 32-bit creation and modification times. + skip(src, 8)?; + + // 32 bit duration. + let timescale = be_u32(src)?; + let duration = { + // Since we convert the 32-bit duration to 64-bit by + // upcasting, we need to preserve the special all-1s + // ("unknown") case by hand. + let d = be_u32(src)?; + if d == std::u32::MAX { + std::u64::MAX + } else { + u64::from(d) + } + }; + (timescale, duration) + } + _ => return Err(Error::InvalidData("unhandled mdhd version")), + }; + + // Skip uninteresting fields. + skip(src, 4)?; + + Ok(MediaHeaderBox { + timescale, + duration, + }) +} + +/// Parse a stco box. +/// See ISOBMFF (ISO 14496-12:2020) § 8.7.5 +fn read_stco(src: &mut BMFFBox) -> Result { + let (_, _) = read_fullbox_extra(src)?; + let offset_count = be_u32(src)?; + let mut offsets = TryVec::with_capacity(offset_count.to_usize())?; + for _ in 0..offset_count { + offsets.push(be_u32(src)?.into())?; + } + + // Padding could be added in some contents. + skip_box_remain(src)?; + + Ok(ChunkOffsetBox { offsets }) +} + +/// Parse a co64 box. +/// See ISOBMFF (ISO 14496-12:2020) § 8.7.5 +fn read_co64(src: &mut BMFFBox) -> Result { + let (_, _) = read_fullbox_extra(src)?; + let offset_count = be_u32(src)?; + let mut offsets = TryVec::with_capacity(offset_count.to_usize())?; + for _ in 0..offset_count { + offsets.push(be_u64(src)?)?; + } + + // Padding could be added in some contents. + skip_box_remain(src)?; + + Ok(ChunkOffsetBox { offsets }) +} + +/// Parse a stss box. +/// See ISOBMFF (ISO 14496-12:2020) § 8.6.2 +fn read_stss(src: &mut BMFFBox) -> Result { + let (_, _) = read_fullbox_extra(src)?; + let sample_count = be_u32(src)?; + let mut samples = TryVec::with_capacity(sample_count.to_usize())?; + for _ in 0..sample_count { + samples.push(be_u32(src)?)?; + } + + // Padding could be added in some contents. + skip_box_remain(src)?; + + Ok(SyncSampleBox { samples }) +} + +/// Parse a stsc box. +/// See ISOBMFF (ISO 14496-12:2020) § 8.7.4 +fn read_stsc(src: &mut BMFFBox) -> Result { + let (_, _) = read_fullbox_extra(src)?; + let sample_count = be_u32(src)?; + let mut samples = TryVec::with_capacity(sample_count.to_usize())?; + for _ in 0..sample_count { + let first_chunk = be_u32(src)?; + let samples_per_chunk = be_u32(src)?; + let sample_description_index = be_u32(src)?; + samples.push(SampleToChunk { + first_chunk, + samples_per_chunk, + sample_description_index, + })?; + } + + // Padding could be added in some contents. + skip_box_remain(src)?; + + Ok(SampleToChunkBox { samples }) +} + +/// Parse a Composition Time to Sample Box +/// See ISOBMFF (ISO 14496-12:2020) § 8.6.1.3 +fn read_ctts(src: &mut BMFFBox) -> Result { + let (version, _) = read_fullbox_extra(src)?; + + let counts = be_u32(src)?; + + if counts + .checked_mul(8) + .map_or(true, |bytes| u64::from(bytes) > src.bytes_left()) + { + return Err(Error::InvalidData("insufficient data in 'ctts' box")); + } + + let mut offsets = TryVec::with_capacity(counts.to_usize())?; + for _ in 0..counts { + let (sample_count, time_offset) = match version { + // According to spec, Version0 shoule be used when version == 0; + // however, some buggy contents have negative value when version == 0. + // So we always use Version1 here. + 0..=1 => { + let count = be_u32(src)?; + let offset = TimeOffsetVersion::Version1(be_i32(src)?); + (count, offset) + } + _ => { + return Err(Error::InvalidData("unsupported version in 'ctts' box")); + } + }; + offsets.push(TimeOffset { + sample_count, + time_offset, + })?; + } + + check_parser_state!(src.content); + + Ok(CompositionOffsetBox { samples: offsets }) +} + +/// Parse a stsz box. +/// See ISOBMFF (ISO 14496-12:2020) § 8.7.3.2 +fn read_stsz(src: &mut BMFFBox) -> Result { + let (_, _) = read_fullbox_extra(src)?; + let sample_size = be_u32(src)?; + let sample_count = be_u32(src)?; + let mut sample_sizes = TryVec::new(); + if sample_size == 0 { + sample_sizes.reserve(sample_count.to_usize())?; + for _ in 0..sample_count { + sample_sizes.push(be_u32(src)?)?; + } + } + + // Padding could be added in some contents. + skip_box_remain(src)?; + + Ok(SampleSizeBox { + sample_size, + sample_sizes, + }) +} + +/// Parse a stts box. +/// See ISOBMFF (ISO 14496-12:2020) § 8.6.1.2 +fn read_stts(src: &mut BMFFBox) -> Result { + let (_, _) = read_fullbox_extra(src)?; + let sample_count = be_u32(src)?; + let mut samples = TryVec::with_capacity(sample_count.to_usize())?; + for _ in 0..sample_count { + let sample_count = be_u32(src)?; + let sample_delta = be_u32(src)?; + samples.push(Sample { + sample_count, + sample_delta, + })?; + } + + // Padding could be added in some contents. + skip_box_remain(src)?; + + Ok(TimeToSampleBox { samples }) +} + +/// Parse a VPx Config Box. +fn read_vpcc(src: &mut BMFFBox) -> Result { + let (version, _) = read_fullbox_extra(src)?; + let supported_versions = [0, 1]; + if !supported_versions.contains(&version) { + return Err(Error::Unsupported("unknown vpcC version")); + } + + let profile = src.read_u8()?; + let level = src.read_u8()?; + let ( + bit_depth, + colour_primaries, + chroma_subsampling, + transfer_characteristics, + matrix_coefficients, + video_full_range_flag, + ) = if version == 0 { + let (bit_depth, colour_primaries) = { + let byte = src.read_u8()?; + ((byte >> 4) & 0x0f, byte & 0x0f) + }; + // Note, transfer_characteristics was known as transfer_function in v0 + let (chroma_subsampling, transfer_characteristics, video_full_range_flag) = { + let byte = src.read_u8()?; + ((byte >> 4) & 0x0f, (byte >> 1) & 0x07, (byte & 1) == 1) + }; + ( + bit_depth, + colour_primaries, + chroma_subsampling, + transfer_characteristics, + None, + video_full_range_flag, + ) + } else { + let (bit_depth, chroma_subsampling, video_full_range_flag) = { + let byte = src.read_u8()?; + ((byte >> 4) & 0x0f, (byte >> 1) & 0x07, (byte & 1) == 1) + }; + let colour_primaries = src.read_u8()?; + let transfer_characteristics = src.read_u8()?; + let matrix_coefficients = src.read_u8()?; + + ( + bit_depth, + colour_primaries, + chroma_subsampling, + transfer_characteristics, + Some(matrix_coefficients), + video_full_range_flag, + ) + }; + + let codec_init_size = be_u16(src)?; + let codec_init = read_buf(src, codec_init_size.into())?; + + // TODO(rillian): validate field value ranges. + Ok(VPxConfigBox { + profile, + level, + bit_depth, + colour_primaries, + chroma_subsampling, + transfer_characteristics, + matrix_coefficients, + video_full_range_flag, + codec_init, + }) +} + +/// See [AV1-ISOBMFF § 2.3.3](https://aomediacodec.github.io/av1-isobmff/#av1codecconfigurationbox-syntax) +fn read_av1c(src: &mut BMFFBox) -> Result { + // We want to store the raw config as well as a structured (parsed) config, so create a copy of + // the raw config so we have it later, and then parse the structured data from that. + let raw_config = src.read_into_try_vec()?; + let mut raw_config_slice = raw_config.as_slice(); + let marker_byte = raw_config_slice.read_u8()?; + if marker_byte & 0x80 != 0x80 { + return Err(Error::Unsupported("missing av1C marker bit")); + } + if marker_byte & 0x7f != 0x01 { + return Err(Error::Unsupported("missing av1C marker bit")); + } + let profile_byte = raw_config_slice.read_u8()?; + let profile = (profile_byte & 0xe0) >> 5; + let level = profile_byte & 0x1f; + let flags_byte = raw_config_slice.read_u8()?; + let tier = (flags_byte & 0x80) >> 7; + let bit_depth = match flags_byte & 0x60 { + 0x60 => 12, + 0x40 => 10, + _ => 8, + }; + let monochrome = flags_byte & 0x10 == 0x10; + let chroma_subsampling_x = (flags_byte & 0x08) >> 3; + let chroma_subsampling_y = (flags_byte & 0x04) >> 2; + let chroma_sample_position = flags_byte & 0x03; + let delay_byte = raw_config_slice.read_u8()?; + let initial_presentation_delay_present = (delay_byte & 0x10) == 0x10; + let initial_presentation_delay_minus_one = if initial_presentation_delay_present { + delay_byte & 0x0f + } else { + 0 + }; + + Ok(AV1ConfigBox { + profile, + level, + tier, + bit_depth, + monochrome, + chroma_subsampling_x, + chroma_subsampling_y, + chroma_sample_position, + initial_presentation_delay_present, + initial_presentation_delay_minus_one, + raw_config, + }) +} + +fn read_flac_metadata(src: &mut BMFFBox) -> Result { + let temp = src.read_u8()?; + let block_type = temp & 0x7f; + let length = be_u24(src)?.into(); + if length > src.bytes_left() { + return Err(Error::InvalidData( + "FLACMetadataBlock larger than parent box", + )); + } + let data = read_buf(src, length)?; + Ok(FLACMetadataBlock { block_type, data }) +} + +/// See MPEG-4 Systems (ISO 14496-1:2010) § 7.2.6.5 +fn find_descriptor(data: &[u8], esds: &mut ES_Descriptor) -> Result<()> { + // Tags for elementary stream description + const ESDESCR_TAG: u8 = 0x03; + const DECODER_CONFIG_TAG: u8 = 0x04; + const DECODER_SPECIFIC_TAG: u8 = 0x05; + + let mut remains = data; + + // Descriptor length should be more than 2 bytes. + while remains.len() > 2 { + let des = &mut Cursor::new(remains); + let tag = des.read_u8()?; + + // See MPEG-4 Systems (ISO 14496-1:2010) § 8.3.3 for interpreting size of expandable classes + + let mut end: u32 = 0; // It's u8 without declaration type that is incorrect. + // MSB of extend_or_len indicates more bytes, up to 4 bytes. + for _ in 0..4 { + if des.position() == remains.len().to_u64() { + // There's nothing more to read, the 0x80 was actually part of + // the content, and not an extension size. + end = des.position() as u32; + break; + } + let extend_or_len = des.read_u8()?; + end = (end << 7) + u32::from(extend_or_len & 0x7F); + if (extend_or_len & 0b1000_0000) == 0 { + end += des.position() as u32; + break; + } + } + + if end.to_usize() > remains.len() || u64::from(end) < des.position() { + return Err(Error::InvalidData("Invalid descriptor.")); + } + + let descriptor = &remains[des.position().try_into()?..end.to_usize()]; + + match tag { + ESDESCR_TAG => { + read_es_descriptor(descriptor, esds)?; + } + DECODER_CONFIG_TAG => { + read_dc_descriptor(descriptor, esds)?; + } + DECODER_SPECIFIC_TAG => { + read_ds_descriptor(descriptor, esds)?; + } + _ => { + debug!("Unsupported descriptor, tag {}", tag); + } + } + + remains = &remains[end.to_usize()..remains.len()]; + debug!("remains.len(): {}", remains.len()); + } + + Ok(()) +} + +fn get_audio_object_type(bit_reader: &mut BitReader) -> Result { + let mut audio_object_type: u16 = ReadInto::read(bit_reader, 5)?; + + // Extend audio object type, for example, HE-AAC. + if audio_object_type == 31 { + let audio_object_type_ext: u16 = ReadInto::read(bit_reader, 6)?; + audio_object_type = 32 + audio_object_type_ext; + } + Ok(audio_object_type) +} + +/// See MPEG-4 Systems (ISO 14496-1:2010) § 7.2.6.7 and probably 14496-3 somewhere? +fn read_ds_descriptor(data: &[u8], esds: &mut ES_Descriptor) -> Result<()> { + #[cfg(feature = "mp4v")] + // Check if we are in a Visual esda Box. + if esds.video_codec != CodecType::Unknown { + esds.decoder_specific_data.extend_from_slice(data)?; + return Ok(()); + } + + // We are in an Audio esda Box. + let frequency_table = vec![ + (0x0, 96000), + (0x1, 88200), + (0x2, 64000), + (0x3, 48000), + (0x4, 44100), + (0x5, 32000), + (0x6, 24000), + (0x7, 22050), + (0x8, 16000), + (0x9, 12000), + (0xa, 11025), + (0xb, 8000), + (0xc, 7350), + ]; + + let bit_reader = &mut BitReader::new(data); + + let mut audio_object_type = get_audio_object_type(bit_reader)?; + + let sample_index: u32 = ReadInto::read(bit_reader, 4)?; + + // Sample frequency could be from table, or retrieved from stream directly + // if index is 0x0f. + let sample_frequency = match sample_index { + 0x0F => Some(ReadInto::read(bit_reader, 24)?), + _ => frequency_table + .iter() + .find(|item| item.0 == sample_index) + .map(|x| x.1), + }; + + let channel_configuration: u16 = ReadInto::read(bit_reader, 4)?; + + let extended_audio_object_type = match audio_object_type { + 5 | 29 => Some(5), + _ => None, + }; + + if audio_object_type == 5 || audio_object_type == 29 { + // We have an explicit signaling for BSAC extension, should the decoder + // decode the BSAC extension (all Gecko's AAC decoders do), then this is + // what the stream will actually look like once decoded. + let _extended_sample_index = ReadInto::read(bit_reader, 4)?; + let _extended_sample_frequency: Option = match _extended_sample_index { + 0x0F => Some(ReadInto::read(bit_reader, 24)?), + _ => frequency_table + .iter() + .find(|item| item.0 == sample_index) + .map(|x| x.1), + }; + audio_object_type = get_audio_object_type(bit_reader)?; + let _extended_channel_configuration = match audio_object_type { + 22 => ReadInto::read(bit_reader, 4)?, + _ => channel_configuration, + }; + }; + + match audio_object_type { + 1..=4 | 6 | 7 | 17 | 19..=23 => { + if sample_frequency.is_none() { + return Err(Error::Unsupported("unknown frequency")); + } + + // parsing GASpecificConfig + + // If the sampling rate is not one of the rates listed in the right + // column in Table 4.82, the sampling frequency dependent tables + // (code tables, scale factor band tables etc.) must be deduced in + // order for the bitstream payload to be parsed. Since a given + // sampling frequency is associated with only one sampling frequency + // table, and since maximum flexibility is desired in the range of + // possible sampling frequencies, the following table shall be used + // to associate an implied sampling frequency with the desired + // sampling frequency dependent tables. + let sample_frequency_value = match sample_frequency.unwrap() { + 0..=9390 => 8000, + 9391..=11501 => 11025, + 11502..=13855 => 12000, + 13856..=18782 => 16000, + 18783..=23003 => 22050, + 23004..=27712 => 24000, + 27713..=37565 => 32000, + 37566..=46008 => 44100, + 46009..=55425 => 48000, + 55426..=75131 => 64000, + 75132..=92016 => 88200, + _ => 96000, + }; + + bit_reader.skip(1)?; // frameLengthFlag + let depend_on_core_order: u8 = ReadInto::read(bit_reader, 1)?; + if depend_on_core_order > 0 { + bit_reader.skip(14)?; // codeCoderDelay + } + bit_reader.skip(1)?; // extensionFlag + + let channel_counts = match channel_configuration { + 0 => { + debug!("Parsing program_config_element for channel counts"); + + bit_reader.skip(4)?; // element_instance_tag + bit_reader.skip(2)?; // object_type + bit_reader.skip(4)?; // sampling_frequency_index + let num_front_channel: u8 = ReadInto::read(bit_reader, 4)?; + let num_side_channel: u8 = ReadInto::read(bit_reader, 4)?; + let num_back_channel: u8 = ReadInto::read(bit_reader, 4)?; + let num_lfe_channel: u8 = ReadInto::read(bit_reader, 2)?; + bit_reader.skip(3)?; // num_assoc_data + bit_reader.skip(4)?; // num_valid_cc + + let mono_mixdown_present: bool = ReadInto::read(bit_reader, 1)?; + if mono_mixdown_present { + bit_reader.skip(4)?; // mono_mixdown_element_number + } + + let stereo_mixdown_present: bool = ReadInto::read(bit_reader, 1)?; + if stereo_mixdown_present { + bit_reader.skip(4)?; // stereo_mixdown_element_number + } + + let matrix_mixdown_idx_present: bool = ReadInto::read(bit_reader, 1)?; + if matrix_mixdown_idx_present { + bit_reader.skip(2)?; // matrix_mixdown_idx + bit_reader.skip(1)?; // pseudo_surround_enable + } + let mut _channel_counts = 0; + _channel_counts += read_surround_channel_count(bit_reader, num_front_channel)?; + _channel_counts += read_surround_channel_count(bit_reader, num_side_channel)?; + _channel_counts += read_surround_channel_count(bit_reader, num_back_channel)?; + _channel_counts += read_surround_channel_count(bit_reader, num_lfe_channel)?; + _channel_counts + } + 1..=7 => channel_configuration, + // Amendment 4 of the AAC standard in 2013 below + 11 => 7, // 6.1 Amendment 4 of the AAC standard in 2013 + 12 | 14 => 8, // 7.1 (a/d) of ITU BS.2159 + _ => { + return Err(Error::Unsupported("invalid channel configuration")); + } + }; + + esds.audio_object_type = Some(audio_object_type); + esds.extended_audio_object_type = extended_audio_object_type; + esds.audio_sample_rate = Some(sample_frequency_value); + esds.audio_channel_count = Some(channel_counts); + if !esds.decoder_specific_data.is_empty() { + return Err(Error::InvalidData( + "There can be only one DecSpecificInfoTag descriptor", + )); + } + esds.decoder_specific_data.extend_from_slice(data)?; + + Ok(()) + } + _ => Err(Error::Unsupported("unknown aac audio object type")), + } +} + +fn read_surround_channel_count(bit_reader: &mut BitReader, channels: u8) -> Result { + let mut count = 0; + for _ in 0..channels { + let is_cpe: bool = ReadInto::read(bit_reader, 1)?; + count += if is_cpe { 2 } else { 1 }; + bit_reader.skip(4)?; + } + Ok(count) +} + +/// See MPEG-4 Systems (ISO 14496-1:2010) § 7.2.6.6 +fn read_dc_descriptor(data: &[u8], esds: &mut ES_Descriptor) -> Result<()> { + let des = &mut Cursor::new(data); + let object_profile = des.read_u8()?; + + #[cfg(feature = "mp4v")] + { + esds.video_codec = match object_profile { + 0x20..=0x24 => CodecType::MP4V, + _ => CodecType::Unknown, + }; + } + + // Skip uninteresting fields. + skip(des, 12)?; + + if data.len().to_u64() > des.position() { + find_descriptor(&data[des.position().try_into()?..data.len()], esds)?; + } + + esds.audio_codec = match object_profile { + 0x40 | 0x41 => CodecType::AAC, + 0x69 | 0x6B => CodecType::MP3, + _ => CodecType::Unknown, + }; + + debug!( + "read_dc_descriptor: esds.audio_codec = {:?}", + esds.audio_codec + ); + + Ok(()) +} + +/// See MPEG-4 Systems (ISO 14496-1:2010) § 7.2.6.5 +fn read_es_descriptor(data: &[u8], esds: &mut ES_Descriptor) -> Result<()> { + let des = &mut Cursor::new(data); + + skip(des, 2)?; + + let esds_flags = des.read_u8()?; + + // Stream dependency flag, first bit from left most. + if esds_flags & 0x80 > 0 { + // Skip uninteresting fields. + skip(des, 2)?; + } + + // Url flag, second bit from left most. + if esds_flags & 0x40 > 0 { + // Skip uninteresting fields. + let skip_es_len = u64::from(des.read_u8()?) + 2; + skip(des, skip_es_len)?; + } + + if data.len().to_u64() > des.position() { + find_descriptor(&data[des.position().try_into()?..data.len()], esds)?; + } + + Ok(()) +} + +/// See MP4 (ISO 14496-14:2020) § 6.7.2 +fn read_esds(src: &mut BMFFBox) -> Result { + let (_, _) = read_fullbox_extra(src)?; + + let esds_array = read_buf(src, src.bytes_left())?; + + let mut es_data = ES_Descriptor::default(); + find_descriptor(&esds_array, &mut es_data)?; + + es_data.codec_esds = esds_array; + + Ok(es_data) +} + +/// Parse `FLACSpecificBox`. +/// See [Encapsulation of FLAC in ISO Base Media File Format](https://github.com/xiph/flac/blob/master/doc/isoflac.txt) § 3.3.2 +fn read_dfla(src: &mut BMFFBox) -> Result { + let (version, flags) = read_fullbox_extra(src)?; + if version != 0 { + return Err(Error::Unsupported("unknown dfLa (FLAC) version")); + } + if flags != 0 { + return Err(Error::InvalidData("no-zero dfLa (FLAC) flags")); + } + let mut blocks = TryVec::new(); + while src.bytes_left() > 0 { + let block = read_flac_metadata(src)?; + blocks.push(block)?; + } + // The box must have at least one meta block, and the first block + // must be the METADATA_BLOCK_STREAMINFO + if blocks.is_empty() { + return Err(Error::InvalidData("FLACSpecificBox missing metadata")); + } else if blocks[0].block_type != 0 { + return Err(Error::InvalidData( + "FLACSpecificBox must have STREAMINFO metadata first", + )); + } else if blocks[0].data.len() != 34 { + return Err(Error::InvalidData( + "FLACSpecificBox STREAMINFO block is the wrong size", + )); + } + Ok(FLACSpecificBox { version, blocks }) +} + +/// Parse `OpusSpecificBox`. +fn read_dops(src: &mut BMFFBox) -> Result { + let version = src.read_u8()?; + if version != 0 { + return Err(Error::Unsupported("unknown dOps (Opus) version")); + } + + let output_channel_count = src.read_u8()?; + let pre_skip = be_u16(src)?; + let input_sample_rate = be_u32(src)?; + let output_gain = be_i16(src)?; + let channel_mapping_family = src.read_u8()?; + + let channel_mapping_table = if channel_mapping_family == 0 { + None + } else { + let stream_count = src.read_u8()?; + let coupled_count = src.read_u8()?; + let channel_mapping = read_buf(src, output_channel_count.into())?; + + Some(ChannelMappingTable { + stream_count, + coupled_count, + channel_mapping, + }) + }; + + // TODO(kinetik): validate field value ranges. + Ok(OpusSpecificBox { + version, + output_channel_count, + pre_skip, + input_sample_rate, + output_gain, + channel_mapping_family, + channel_mapping_table, + }) +} + +/// Re-serialize the Opus codec-specific config data as an `OpusHead` packet. +/// +/// Some decoders expect the initialization data in the format used by the +/// Ogg and WebM encapsulations. To support this we prepend the `OpusHead` +/// tag and byte-swap the data from big- to little-endian relative to the +/// dOps box. +pub fn serialize_opus_header( + opus: &OpusSpecificBox, + dst: &mut W, +) -> Result<()> { + match dst.write(b"OpusHead") { + Err(e) => return Err(Error::from(e)), + Ok(bytes) => { + if bytes != 8 { + return Err(Error::InvalidData("Couldn't write OpusHead tag.")); + } + } + } + // In mp4 encapsulation, the version field is 0, but in ogg + // it is 1. While decoders generally accept zero as well, write + // out the version of the header we're supporting rather than + // whatever we parsed out of mp4. + dst.write_u8(1)?; + dst.write_u8(opus.output_channel_count)?; + dst.write_u16::(opus.pre_skip)?; + dst.write_u32::(opus.input_sample_rate)?; + dst.write_i16::(opus.output_gain)?; + dst.write_u8(opus.channel_mapping_family)?; + match opus.channel_mapping_table { + None => {} + Some(ref table) => { + dst.write_u8(table.stream_count)?; + dst.write_u8(table.coupled_count)?; + match dst.write(&table.channel_mapping) { + Err(e) => return Err(Error::from(e)), + Ok(bytes) => { + if bytes != table.channel_mapping.len() { + return Err(Error::InvalidData( + "Couldn't write channel mapping table data.", + )); + } + } + } + } + }; + Ok(()) +} + +/// Parse `ALACSpecificBox`. +fn read_alac(src: &mut BMFFBox) -> Result { + let (version, flags) = read_fullbox_extra(src)?; + if version != 0 { + return Err(Error::Unsupported("unknown alac (ALAC) version")); + } + if flags != 0 { + return Err(Error::InvalidData("no-zero alac (ALAC) flags")); + } + + let length = match src.bytes_left() { + x @ 24 | x @ 48 => x, + _ => { + return Err(Error::InvalidData( + "ALACSpecificBox magic cookie is the wrong size", + )) + } + }; + let data = read_buf(src, length)?; + + Ok(ALACSpecificBox { version, data }) +} + +/// Parse a Handler Reference Box.
+/// See ISOBMFF (ISO 14496-12:2020) § 8.4.3
+/// See [\[ISOBMFF\]: reserved (field = 0;) handling is ambiguous](https://github.com/MPEGGroup/FileFormat/issues/36) +fn read_hdlr(src: &mut BMFFBox, strictness: ParseStrictness) -> Result { + if read_fullbox_version_no_flags(src)? != 0 { + return Err(Error::Unsupported("hdlr version")); + } + + let pre_defined = be_u32(src)?; + if pre_defined != 0 { + fail_if( + strictness == ParseStrictness::Strict, + "The HandlerBox 'pre_defined' field shall be 0 \ + per ISOBMFF (ISO 14496-12:2020) § 8.4.3.2", + )?; + } + + let handler_type = FourCC::from(be_u32(src)?); + + for _ in 1..=3 { + let reserved = be_u32(src)?; + if reserved != 0 { + fail_if( + strictness == ParseStrictness::Strict, + "The HandlerBox 'reserved' fields shall be 0 \ + per ISOBMFF (ISO 14496-12:2020) § 8.4.3.2", + )?; + } + } + + match std::str::from_utf8(src.read_into_try_vec()?.as_slice()) { + Ok(name) => { + match name.bytes().filter(|&b| b == b'\0').count() { + 0 => fail_if( + strictness != ParseStrictness::Permissive, + "The HandlerBox 'name' field shall be null-terminated \ + per ISOBMFF (ISO 14496-12:2020) § 8.4.3.2", + )?, + 1 => (), + _ => + // See https://github.com/MPEGGroup/FileFormat/issues/35 + { + fail_if( + strictness == ParseStrictness::Strict, + "The HandlerBox 'name' field shall have a NUL byte \ + only in the final position \ + per ISOBMFF (ISO 14496-12:2020) § 8.4.3.2", + )? + } + } + } + Err(_) => fail_if( + strictness != ParseStrictness::Permissive, + "The HandlerBox 'name' field shall be valid utf8 \ + per ISOBMFF (ISO 14496-12:2020) § 8.4.3.2", + )?, + } + + Ok(HandlerBox { handler_type }) +} + +/// Parse an video description inside an stsd box. +fn read_video_sample_entry(src: &mut BMFFBox) -> Result { + let name = src.get_header().name; + let codec_type = match name { + BoxType::AVCSampleEntry | BoxType::AVC3SampleEntry => CodecType::H264, + BoxType::MP4VideoSampleEntry => CodecType::MP4V, + BoxType::VP8SampleEntry => CodecType::VP8, + BoxType::VP9SampleEntry => CodecType::VP9, + BoxType::AV1SampleEntry => CodecType::AV1, + BoxType::ProtectedVisualSampleEntry => CodecType::EncryptedVideo, + BoxType::H263SampleEntry => CodecType::H263, + _ => { + debug!("Unsupported video codec, box {:?} found", name); + CodecType::Unknown + } + }; + + // Skip uninteresting fields. + skip(src, 6)?; + + let data_reference_index = be_u16(src)?; + + // Skip uninteresting fields. + skip(src, 16)?; + + let width = be_u16(src)?; + let height = be_u16(src)?; + + // Skip uninteresting fields. + skip(src, 50)?; + + // Skip clap/pasp/etc. for now. + let mut codec_specific = None; + let mut protection_info = TryVec::new(); + let mut iter = src.box_iter(); + while let Some(mut b) = iter.next_box()? { + match b.head.name { + BoxType::AVCConfigurationBox => { + if (name != BoxType::AVCSampleEntry + && name != BoxType::AVC3SampleEntry + && name != BoxType::ProtectedVisualSampleEntry) + || codec_specific.is_some() + { + return Err(Error::InvalidData("malformed video sample entry")); + } + let avcc_size = b + .head + .size + .checked_sub(b.head.offset) + .expect("offset invalid"); + let avcc = read_buf(&mut b.content, avcc_size)?; + debug!("{:?} (avcc)", avcc); + // TODO(kinetik): Parse avcC box? For now we just stash the data. + codec_specific = Some(VideoCodecSpecific::AVCConfig(avcc)); + } + BoxType::H263SpecificBox => { + if (name != BoxType::H263SampleEntry) || codec_specific.is_some() { + return Err(Error::InvalidData("malformed video sample entry")); + } + let h263_dec_spec_struc_size = b + .head + .size + .checked_sub(b.head.offset) + .expect("offset invalid"); + let h263_dec_spec_struc = read_buf(&mut b.content, h263_dec_spec_struc_size)?; + debug!("{:?} (h263DecSpecStruc)", h263_dec_spec_struc); + + codec_specific = Some(VideoCodecSpecific::H263Config(h263_dec_spec_struc)); + } + BoxType::VPCodecConfigurationBox => { + // vpcC + if (name != BoxType::VP8SampleEntry + && name != BoxType::VP9SampleEntry + && name != BoxType::ProtectedVisualSampleEntry) + || codec_specific.is_some() + { + return Err(Error::InvalidData("malformed video sample entry")); + } + let vpcc = read_vpcc(&mut b)?; + codec_specific = Some(VideoCodecSpecific::VPxConfig(vpcc)); + } + BoxType::AV1CodecConfigurationBox => { + if name != BoxType::AV1SampleEntry && name != BoxType::ProtectedVisualSampleEntry { + return Err(Error::InvalidData("malformed video sample entry")); + } + let av1c = read_av1c(&mut b)?; + codec_specific = Some(VideoCodecSpecific::AV1Config(av1c)); + } + BoxType::ESDBox => { + if name != BoxType::MP4VideoSampleEntry || codec_specific.is_some() { + return Err(Error::InvalidData("malformed video sample entry")); + } + #[cfg(not(feature = "mp4v"))] + { + let (_, _) = read_fullbox_extra(&mut b.content)?; + // Subtract 4 extra to offset the members of fullbox not + // accounted for in head.offset + let esds_size = b + .head + .size + .checked_sub(b.head.offset + 4) + .expect("offset invalid"); + let esds = read_buf(&mut b.content, esds_size)?; + codec_specific = Some(VideoCodecSpecific::ESDSConfig(esds)); + } + #[cfg(feature = "mp4v")] + { + // Read ES_Descriptor inside an esds box. + // See ISOBMFF (ISO 14496-1:2010 §7.2.6.5) + let esds = read_esds(&mut b)?; + codec_specific = + Some(VideoCodecSpecific::ESDSConfig(esds.decoder_specific_data)); + } + } + BoxType::ProtectionSchemeInfoBox => { + if name != BoxType::ProtectedVisualSampleEntry { + return Err(Error::InvalidData("malformed video sample entry")); + } + let sinf = read_sinf(&mut b)?; + debug!("{:?} (sinf)", sinf); + protection_info.push(sinf)?; + } + _ => { + debug!("Unsupported video codec, box {:?} found", b.head.name); + skip_box_content(&mut b)?; + } + } + check_parser_state!(b.content); + } + + Ok( + codec_specific.map_or(SampleEntry::Unknown, |codec_specific| { + SampleEntry::Video(VideoSampleEntry { + codec_type, + data_reference_index, + width, + height, + codec_specific, + protection_info, + }) + }), + ) +} + +fn read_qt_wave_atom(src: &mut BMFFBox) -> Result { + let mut codec_specific = None; + let mut iter = src.box_iter(); + while let Some(mut b) = iter.next_box()? { + match b.head.name { + BoxType::ESDBox => { + let esds = read_esds(&mut b)?; + codec_specific = Some(esds); + } + _ => skip_box_content(&mut b)?, + } + } + + codec_specific.ok_or(Error::InvalidData("malformed audio sample entry")) +} + +/// Parse an audio description inside an stsd box. +/// See ISOBMFF (ISO 14496-12:2020) § 12.2.3 +fn read_audio_sample_entry(src: &mut BMFFBox) -> Result { + let name = src.get_header().name; + + // Skip uninteresting fields. + skip(src, 6)?; + + let data_reference_index = be_u16(src)?; + + // XXX(kinetik): This is "reserved" in BMFF, but some old QT MOV variant + // uses it, need to work out if we have to support it. Without checking + // here and reading extra fields after samplerate (or bailing with an + // error), the parser loses sync completely. + let version = be_u16(src)?; + + // Skip uninteresting fields. + skip(src, 6)?; + + let mut channelcount = u32::from(be_u16(src)?); + let samplesize = be_u16(src)?; + + // Skip uninteresting fields. + skip(src, 4)?; + + let mut samplerate = f64::from(be_u32(src)? >> 16); // 16.16 fixed point; + + match version { + 0 => (), + 1 => { + // Quicktime sound sample description version 1. + // Skip uninteresting fields. + skip(src, 16)?; + } + 2 => { + // Quicktime sound sample description version 2. + skip(src, 4)?; + samplerate = f64::from_bits(be_u64(src)?); + channelcount = be_u32(src)?; + skip(src, 20)?; + } + _ => { + return Err(Error::Unsupported( + "unsupported non-isom audio sample entry", + )) + } + } + + let (mut codec_type, mut codec_specific) = match name { + BoxType::MP3AudioSampleEntry => (CodecType::MP3, Some(AudioCodecSpecific::MP3)), + BoxType::LPCMAudioSampleEntry => (CodecType::LPCM, Some(AudioCodecSpecific::LPCM)), + // Some mp4 file with AMR doesn't have AMRSpecificBox "damr" in followed while loop, + // we use empty box by default. + #[cfg(feature = "3gpp")] + BoxType::AMRNBSampleEntry => ( + CodecType::AMRNB, + Some(AudioCodecSpecific::AMRSpecificBox(Default::default())), + ), + #[cfg(feature = "3gpp")] + BoxType::AMRWBSampleEntry => ( + CodecType::AMRWB, + Some(AudioCodecSpecific::AMRSpecificBox(Default::default())), + ), + _ => (CodecType::Unknown, None), + }; + let mut protection_info = TryVec::new(); + let mut iter = src.box_iter(); + while let Some(mut b) = iter.next_box()? { + match b.head.name { + BoxType::ESDBox => { + if (name != BoxType::MP4AudioSampleEntry + && name != BoxType::ProtectedAudioSampleEntry) + || codec_specific.is_some() + { + return Err(Error::InvalidData("malformed audio sample entry")); + } + let esds = read_esds(&mut b)?; + codec_type = esds.audio_codec; + codec_specific = Some(AudioCodecSpecific::ES_Descriptor(esds)); + } + BoxType::FLACSpecificBox => { + if (name != BoxType::FLACSampleEntry && name != BoxType::ProtectedAudioSampleEntry) + || codec_specific.is_some() + { + return Err(Error::InvalidData("malformed audio sample entry")); + } + let dfla = read_dfla(&mut b)?; + codec_type = CodecType::FLAC; + codec_specific = Some(AudioCodecSpecific::FLACSpecificBox(dfla)); + } + BoxType::OpusSpecificBox => { + if (name != BoxType::OpusSampleEntry && name != BoxType::ProtectedAudioSampleEntry) + || codec_specific.is_some() + { + return Err(Error::InvalidData("malformed audio sample entry")); + } + let dops = read_dops(&mut b)?; + codec_type = CodecType::Opus; + codec_specific = Some(AudioCodecSpecific::OpusSpecificBox(dops)); + } + BoxType::ALACSpecificBox => { + if name != BoxType::ALACSpecificBox || codec_specific.is_some() { + return Err(Error::InvalidData("malformed audio sample entry")); + } + let alac = read_alac(&mut b)?; + codec_type = CodecType::ALAC; + codec_specific = Some(AudioCodecSpecific::ALACSpecificBox(alac)); + } + BoxType::QTWaveAtom => { + let qt_esds = read_qt_wave_atom(&mut b)?; + codec_type = qt_esds.audio_codec; + codec_specific = Some(AudioCodecSpecific::ES_Descriptor(qt_esds)); + } + BoxType::ProtectionSchemeInfoBox => { + if name != BoxType::ProtectedAudioSampleEntry { + return Err(Error::InvalidData("malformed audio sample entry")); + } + let sinf = read_sinf(&mut b)?; + debug!("{:?} (sinf)", sinf); + codec_type = CodecType::EncryptedAudio; + protection_info.push(sinf)?; + } + #[cfg(feature = "3gpp")] + BoxType::AMRSpecificBox => { + if codec_type != CodecType::AMRNB && codec_type != CodecType::AMRWB { + return Err(Error::InvalidData("malformed audio sample entry")); + } + let amr_dec_spec_struc_size = b + .head + .size + .checked_sub(b.head.offset) + .expect("offset invalid"); + let amr_dec_spec_struc = read_buf(&mut b.content, amr_dec_spec_struc_size)?; + debug!("{:?} (AMRDecSpecStruc)", amr_dec_spec_struc); + codec_specific = Some(AudioCodecSpecific::AMRSpecificBox(amr_dec_spec_struc)); + } + _ => { + debug!("Unsupported audio codec, box {:?} found", b.head.name); + skip_box_content(&mut b)?; + } + } + check_parser_state!(b.content); + } + + Ok( + codec_specific.map_or(SampleEntry::Unknown, |codec_specific| { + SampleEntry::Audio(AudioSampleEntry { + codec_type, + data_reference_index, + channelcount, + samplesize, + samplerate, + codec_specific, + protection_info, + }) + }), + ) +} + +/// Parse a stsd box. +/// See ISOBMFF (ISO 14496-12:2020) § 8.5.2 +/// See MP4 (ISO 14496-14:2020) § 6.7.2 +fn read_stsd(src: &mut BMFFBox, track: &mut Track) -> Result { + let (_, _) = read_fullbox_extra(src)?; + + let description_count = be_u32(src)?; + let mut descriptions = TryVec::new(); + + { + let mut iter = src.box_iter(); + while let Some(mut b) = iter.next_box()? { + let description = match track.track_type { + TrackType::Video => read_video_sample_entry(&mut b), + TrackType::Audio => read_audio_sample_entry(&mut b), + TrackType::Metadata => Err(Error::Unsupported("metadata track")), + TrackType::Unknown => Err(Error::Unsupported("unknown track type")), + }; + let description = match description { + Ok(desc) => desc, + Err(Error::Unsupported(_)) => { + // read_{audio,video}_desc may have returned Unsupported + // after partially reading the box content, so we can't + // simply use skip_box_content here. + let to_skip = b.bytes_left(); + skip(&mut b, to_skip)?; + SampleEntry::Unknown + } + Err(e) => return Err(e), + }; + descriptions.push(description)?; + check_parser_state!(b.content); + if descriptions.len() == description_count.to_usize() { + break; + } + } + } + + // Padding could be added in some contents. + skip_box_remain(src)?; + + Ok(SampleDescriptionBox { descriptions }) +} + +fn read_sinf(src: &mut BMFFBox) -> Result { + let mut sinf = ProtectionSchemeInfoBox::default(); + + let mut iter = src.box_iter(); + while let Some(mut b) = iter.next_box()? { + match b.head.name { + BoxType::OriginalFormatBox => { + sinf.original_format = FourCC::from(be_u32(&mut b)?); + } + BoxType::SchemeTypeBox => { + sinf.scheme_type = Some(read_schm(&mut b)?); + } + BoxType::SchemeInformationBox => { + // We only need tenc box in schi box so far. + sinf.tenc = read_schi(&mut b)?; + } + _ => skip_box_content(&mut b)?, + } + check_parser_state!(b.content); + } + + Ok(sinf) +} + +fn read_schi(src: &mut BMFFBox) -> Result> { + let mut tenc = None; + let mut iter = src.box_iter(); + while let Some(mut b) = iter.next_box()? { + match b.head.name { + BoxType::TrackEncryptionBox => { + if tenc.is_some() { + return Err(Error::InvalidData( + "tenc box should be only one at most in sinf box", + )); + } + tenc = Some(read_tenc(&mut b)?); + } + _ => skip_box_content(&mut b)?, + } + } + + Ok(tenc) +} + +fn read_tenc(src: &mut BMFFBox) -> Result { + let (version, _) = read_fullbox_extra(src)?; + + // reserved byte + skip(src, 1)?; + // the next byte is used to signal the default pattern in version >= 1 + let (default_crypt_byte_block, default_skip_byte_block) = match version { + 0 => { + skip(src, 1)?; + (None, None) + } + _ => { + let pattern_byte = src.read_u8()?; + let crypt_bytes = pattern_byte >> 4; + let skip_bytes = pattern_byte & 0x0f; + (Some(crypt_bytes), Some(skip_bytes)) + } + }; + let default_is_encrypted = src.read_u8()?; + let default_iv_size = src.read_u8()?; + let default_kid = read_buf(src, 16)?; + // If default_is_encrypted == 1 && default_iv_size == 0 we expect a default_constant_iv + let default_constant_iv = match (default_is_encrypted, default_iv_size) { + (1, 0) => { + let default_constant_iv_size = src.read_u8()?; + Some(read_buf(src, default_constant_iv_size.into())?) + } + _ => None, + }; + + Ok(TrackEncryptionBox { + is_encrypted: default_is_encrypted, + iv_size: default_iv_size, + kid: default_kid, + crypt_byte_block_count: default_crypt_byte_block, + skip_byte_block_count: default_skip_byte_block, + constant_iv: default_constant_iv, + }) +} + +fn read_schm(src: &mut BMFFBox) -> Result { + // Flags can be used to signal presence of URI in the box, but we don't + // use the URI so don't bother storing the flags. + let (_, _) = read_fullbox_extra(src)?; + let scheme_type = FourCC::from(be_u32(src)?); + let scheme_version = be_u32(src)?; + // Null terminated scheme URI may follow, but we don't use it right now. + skip_box_remain(src)?; + Ok(SchemeTypeBox { + scheme_type, + scheme_version, + }) +} + +/// Parse a metadata box inside a moov, trak, or mdia box. +/// See ISOBMFF (ISO 14496-12:2020) § 8.10.1. +fn read_udta(src: &mut BMFFBox) -> Result { + let mut iter = src.box_iter(); + let mut udta = UserdataBox { meta: None }; + + while let Some(mut b) = iter.next_box()? { + match b.head.name { + BoxType::MetadataBox => { + let meta = read_meta(&mut b)?; + udta.meta = Some(meta); + } + _ => skip_box_content(&mut b)?, + }; + check_parser_state!(b.content); + } + Ok(udta) +} + +/// Parse the meta box +/// See ISOBMFF (ISO 14496-12:2020) § 8.11.1 +fn read_meta(src: &mut BMFFBox) -> Result { + let (_, _) = read_fullbox_extra(src)?; + let mut iter = src.box_iter(); + let mut meta = MetadataBox::default(); + while let Some(mut b) = iter.next_box()? { + match b.head.name { + BoxType::MetadataItemListEntry => read_ilst(&mut b, &mut meta)?, + #[cfg(feature = "meta-xml")] + BoxType::MetadataXMLBox => read_xml_(&mut b, &mut meta)?, + #[cfg(feature = "meta-xml")] + BoxType::MetadataBXMLBox => read_bxml(&mut b, &mut meta)?, + _ => skip_box_content(&mut b)?, + }; + check_parser_state!(b.content); + } + Ok(meta) +} + +/// Parse a XML box inside a meta box +/// See ISOBMFF (ISO 14496-12:2020) § 8.11.2 +#[cfg(feature = "meta-xml")] +fn read_xml_(src: &mut BMFFBox, meta: &mut MetadataBox) -> Result<()> { + if read_fullbox_version_no_flags(src)? != 0 { + return Err(Error::Unsupported("unsupported XmlBox version")); + } + meta.xml = Some(XmlBox::StringXmlBox(src.read_into_try_vec()?)); + Ok(()) +} + +/// Parse a Binary XML box inside a meta box +/// See ISOBMFF (ISO 14496-12:2020) § 8.11.2 +#[cfg(feature = "meta-xml")] +fn read_bxml(src: &mut BMFFBox, meta: &mut MetadataBox) -> Result<()> { + if read_fullbox_version_no_flags(src)? != 0 { + return Err(Error::Unsupported("unsupported XmlBox version")); + } + meta.xml = Some(XmlBox::BinaryXmlBox(src.read_into_try_vec()?)); + Ok(()) +} + +/// Parse a metadata box inside a udta box +fn read_ilst(src: &mut BMFFBox, meta: &mut MetadataBox) -> Result<()> { + let mut iter = src.box_iter(); + while let Some(mut b) = iter.next_box()? { + match b.head.name { + BoxType::AlbumEntry => meta.album = read_ilst_string_data(&mut b)?, + BoxType::ArtistEntry | BoxType::ArtistLowercaseEntry => { + meta.artist = read_ilst_string_data(&mut b)? + } + BoxType::AlbumArtistEntry => meta.album_artist = read_ilst_string_data(&mut b)?, + BoxType::CommentEntry => meta.comment = read_ilst_string_data(&mut b)?, + BoxType::DateEntry => meta.year = read_ilst_string_data(&mut b)?, + BoxType::TitleEntry => meta.title = read_ilst_string_data(&mut b)?, + BoxType::CustomGenreEntry => { + meta.genre = read_ilst_string_data(&mut b)?.map(Genre::CustomGenre) + } + BoxType::StandardGenreEntry => { + meta.genre = read_ilst_u8_data(&mut b)? + .and_then(|gnre| Some(Genre::StandardGenre(gnre.get(1).copied()?))) + } + BoxType::ComposerEntry => meta.composer = read_ilst_string_data(&mut b)?, + BoxType::EncoderEntry => meta.encoder = read_ilst_string_data(&mut b)?, + BoxType::EncodedByEntry => meta.encoded_by = read_ilst_string_data(&mut b)?, + BoxType::CopyrightEntry => meta.copyright = read_ilst_string_data(&mut b)?, + BoxType::GroupingEntry => meta.grouping = read_ilst_string_data(&mut b)?, + BoxType::CategoryEntry => meta.category = read_ilst_string_data(&mut b)?, + BoxType::KeywordEntry => meta.keyword = read_ilst_string_data(&mut b)?, + BoxType::PodcastUrlEntry => meta.podcast_url = read_ilst_string_data(&mut b)?, + BoxType::PodcastGuidEntry => meta.podcast_guid = read_ilst_string_data(&mut b)?, + BoxType::DescriptionEntry => meta.description = read_ilst_string_data(&mut b)?, + BoxType::LongDescriptionEntry => meta.long_description = read_ilst_string_data(&mut b)?, + BoxType::LyricsEntry => meta.lyrics = read_ilst_string_data(&mut b)?, + BoxType::TVNetworkNameEntry => meta.tv_network_name = read_ilst_string_data(&mut b)?, + BoxType::TVEpisodeNameEntry => meta.tv_episode_name = read_ilst_string_data(&mut b)?, + BoxType::TVShowNameEntry => meta.tv_show_name = read_ilst_string_data(&mut b)?, + BoxType::PurchaseDateEntry => meta.purchase_date = read_ilst_string_data(&mut b)?, + BoxType::RatingEntry => meta.rating = read_ilst_string_data(&mut b)?, + BoxType::OwnerEntry => meta.owner = read_ilst_string_data(&mut b)?, + BoxType::HDVideoEntry => meta.hd_video = read_ilst_bool_data(&mut b)?, + BoxType::SortNameEntry => meta.sort_name = read_ilst_string_data(&mut b)?, + BoxType::SortArtistEntry => meta.sort_artist = read_ilst_string_data(&mut b)?, + BoxType::SortAlbumEntry => meta.sort_album = read_ilst_string_data(&mut b)?, + BoxType::SortAlbumArtistEntry => { + meta.sort_album_artist = read_ilst_string_data(&mut b)? + } + BoxType::SortComposerEntry => meta.sort_composer = read_ilst_string_data(&mut b)?, + BoxType::TrackNumberEntry => { + if let Some(trkn) = read_ilst_u8_data(&mut b)? { + meta.track_number = trkn.get(3).copied(); + meta.total_tracks = trkn.get(5).copied(); + }; + } + BoxType::DiskNumberEntry => { + if let Some(disk) = read_ilst_u8_data(&mut b)? { + meta.disc_number = disk.get(3).copied(); + meta.total_discs = disk.get(5).copied(); + }; + } + BoxType::TempoEntry => { + meta.beats_per_minute = + read_ilst_u8_data(&mut b)?.and_then(|tmpo| tmpo.get(1).copied()) + } + BoxType::CompilationEntry => meta.compilation = read_ilst_bool_data(&mut b)?, + BoxType::AdvisoryEntry => { + meta.advisory = read_ilst_u8_data(&mut b)?.and_then(|rtng| { + Some(match rtng.get(0)? { + 2 => AdvisoryRating::Clean, + 0 => AdvisoryRating::Inoffensive, + r => AdvisoryRating::Explicit(*r), + }) + }) + } + BoxType::MediaTypeEntry => { + meta.media_type = read_ilst_u8_data(&mut b)?.and_then(|stik| { + Some(match stik.get(0)? { + 0 => MediaType::Movie, + 1 => MediaType::Normal, + 2 => MediaType::AudioBook, + 5 => MediaType::WhackedBookmark, + 6 => MediaType::MusicVideo, + 9 => MediaType::ShortFilm, + 10 => MediaType::TVShow, + 11 => MediaType::Booklet, + s => MediaType::Unknown(*s), + }) + }) + } + BoxType::PodcastEntry => meta.podcast = read_ilst_bool_data(&mut b)?, + BoxType::TVSeasonNumberEntry => { + meta.tv_season = read_ilst_u8_data(&mut b)?.and_then(|tvsn| tvsn.get(3).copied()) + } + BoxType::TVEpisodeNumberEntry => { + meta.tv_episode_number = + read_ilst_u8_data(&mut b)?.and_then(|tves| tves.get(3).copied()) + } + BoxType::GaplessPlaybackEntry => meta.gapless_playback = read_ilst_bool_data(&mut b)?, + BoxType::CoverArtEntry => meta.cover_art = read_ilst_multiple_u8_data(&mut b).ok(), + _ => skip_box_content(&mut b)?, + }; + check_parser_state!(b.content); + } + Ok(()) +} + +fn read_ilst_bool_data(src: &mut BMFFBox) -> Result> { + Ok(read_ilst_u8_data(src)?.and_then(|d| Some(d.get(0)? == &1))) +} + +fn read_ilst_string_data(src: &mut BMFFBox) -> Result> { + read_ilst_u8_data(src) +} + +fn read_ilst_u8_data(src: &mut BMFFBox) -> Result>> { + // For all non-covr atoms, there must only be one data atom. + Ok(read_ilst_multiple_u8_data(src)?.pop()) +} + +fn read_ilst_multiple_u8_data(src: &mut BMFFBox) -> Result>> { + let mut iter = src.box_iter(); + let mut data = TryVec::new(); + while let Some(mut b) = iter.next_box()? { + match b.head.name { + BoxType::MetadataItemDataEntry => { + data.push(read_ilst_data(&mut b)?)?; + } + _ => skip_box_content(&mut b)?, + }; + check_parser_state!(b.content); + } + Ok(data) +} + +fn read_ilst_data(src: &mut BMFFBox) -> Result> { + // Skip past the padding bytes + skip(&mut src.content, src.head.offset)?; + let size = src.content.limit(); + read_buf(&mut src.content, size) +} + +/// Skip a number of bytes that we don't care to parse. +fn skip(src: &mut T, bytes: u64) -> Result<()> { + std::io::copy(&mut src.take(bytes), &mut std::io::sink())?; + Ok(()) +} + +/// Read size bytes into a Vector or return error. +fn read_buf(src: &mut T, size: u64) -> Result> { + let buf = src.take(size).read_into_try_vec()?; + if buf.len().to_u64() != size { + return Err(Error::InvalidData("failed buffer read")); + } + + Ok(buf) +} + +fn be_i16(src: &mut T) -> Result { + src.read_i16::().map_err(From::from) +} + +fn be_i32(src: &mut T) -> Result { + src.read_i32::().map_err(From::from) +} + +fn be_i64(src: &mut T) -> Result { + src.read_i64::().map_err(From::from) +} + +fn be_u16(src: &mut T) -> Result { + src.read_u16::().map_err(From::from) +} + +fn be_u24(src: &mut T) -> Result { + src.read_u24::().map_err(From::from) +} + +fn be_u32(src: &mut T) -> Result { + src.read_u32::().map_err(From::from) +} + +fn be_u64(src: &mut T) -> Result { + src.read_u64::().map_err(From::from) +} + +fn write_be_u32(des: &mut T, num: u32) -> Result<()> { + des.write_u32::(num) + .map_err(From::from) +} diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..a893f7e --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,12 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +macro_rules! check_parser_state { + ( $src:expr ) => { + if $src.limit() > 0 { + debug!("bad parser state: {} content bytes left", $src.limit()); + return Err(Error::InvalidData("unread box content or bad parser sync")); + } + }; +} diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..8087e7c --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,1323 @@ +//! Module for parsing ISO Base Media Format aka video/mp4 streams. +//! Internal unit tests. + +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use super::read_mp4; +use super::Error; +use super::ParseStrictness; +use fallible_collections::TryRead as _; + +use std::convert::TryInto as _; +use std::io::Cursor; +use std::io::Read as _; +extern crate test_assembler; +use self::test_assembler::*; + +use boxes::BoxType; + +enum BoxSize { + Short(u32), + Long(u64), + UncheckedShort(u32), + UncheckedLong(u64), + Auto, +} + +#[allow(clippy::trivially_copy_pass_by_ref)] // TODO: Consider reworking to a copy +fn make_box(size: BoxSize, name: &[u8; 4], func: F) -> Cursor> +where + F: Fn(Section) -> Section, +{ + let mut section = Section::new(); + let box_size = Label::new(); + section = match size { + BoxSize::Short(size) | BoxSize::UncheckedShort(size) => section.B32(size), + BoxSize::Long(_) | BoxSize::UncheckedLong(_) => section.B32(1), + BoxSize::Auto => section.B32(&box_size), + }; + section = section.append_bytes(name); + section = match size { + // The spec allows the 32-bit size to be 0 to indicate unknown + // length streams. It's not clear if this is valid when using a + // 64-bit size, so prohibit it for now. + BoxSize::Long(size) => { + assert!(size > 0); + section.B64(size) + } + BoxSize::UncheckedLong(size) => section.B64(size), + _ => section, + }; + section = func(section); + match size { + BoxSize::Short(size) => { + if size > 0 { + assert_eq!(u64::from(size), section.size()) + } + } + BoxSize::Long(size) => assert_eq!(size, section.size()), + BoxSize::Auto => { + assert!( + section.size() <= u64::from(u32::max_value()), + "Tried to use a long box with BoxSize::Auto" + ); + box_size.set_const(section.size()); + } + // Skip checking BoxSize::Unchecked* cases. + _ => (), + } + Cursor::new(section.get_contents().unwrap()) +} + +fn make_uuid_box(size: BoxSize, uuid: &[u8; 16], func: F) -> Cursor> +where + F: Fn(Section) -> Section, +{ + make_box(size, b"uuid", |mut s| { + for b in uuid { + s = s.B8(*b); + } + func(s) + }) +} + +#[allow(clippy::trivially_copy_pass_by_ref)] // TODO: Consider reworking to a copy +fn make_fullbox(size: BoxSize, name: &[u8; 4], version: u8, func: F) -> Cursor> +where + F: Fn(Section) -> Section, +{ + make_box(size, name, |s| func(s.B8(version).B8(0).B8(0).B8(0))) +} + +#[test] +fn read_box_header_short() { + let mut stream = make_box(BoxSize::Short(8), b"test", |s| s); + let header = super::read_box_header(&mut stream).unwrap(); + assert_eq!(header.name, BoxType::UnknownBox(0x7465_7374)); // "test" + assert_eq!(header.size, 8); + assert!(header.uuid.is_none()); +} + +#[test] +fn read_box_header_long() { + let mut stream = make_box(BoxSize::Long(16), b"test", |s| s); + let header = super::read_box_header(&mut stream).unwrap(); + assert_eq!(header.name, BoxType::UnknownBox(0x7465_7374)); // "test" + assert_eq!(header.size, 16); + assert!(header.uuid.is_none()); +} + +#[test] +fn read_box_header_short_unknown_size() { + let mut stream = make_box(BoxSize::Short(0), b"test", |s| s); + match super::read_box_header(&mut stream) { + Err(Error::Unsupported(s)) => assert_eq!(s, "unknown sized box"), + _ => panic!("unexpected result reading box with unknown size"), + }; +} + +#[test] +fn read_box_header_short_invalid_size() { + let mut stream = make_box(BoxSize::UncheckedShort(2), b"test", |s| s); + match super::read_box_header(&mut stream) { + Err(Error::InvalidData(s)) => assert_eq!(s, "malformed size"), + _ => panic!("unexpected result reading box with invalid size"), + }; +} + +#[test] +fn read_box_header_long_invalid_size() { + let mut stream = make_box(BoxSize::UncheckedLong(2), b"test", |s| s); + match super::read_box_header(&mut stream) { + Err(Error::InvalidData(s)) => assert_eq!(s, "malformed wide size"), + _ => panic!("unexpected result reading box with invalid size"), + }; +} + +#[test] +fn read_box_header_uuid() { + const HEADER_UUID: [u8; 16] = [ + 0x85, 0xc0, 0xb6, 0x87, 0x82, 0x0f, 0x11, 0xe0, 0x81, 0x11, 0xf4, 0xce, 0x46, 0x2b, 0x6a, + 0x48, + ]; + + let mut stream = make_uuid_box(BoxSize::Short(24), &HEADER_UUID, |s| s); + let mut iter = super::BoxIter::new(&mut stream); + let stream = iter.next_box().unwrap().unwrap(); + assert_eq!(stream.head.name, BoxType::UuidBox); + assert_eq!(stream.head.size, 24); + assert!(stream.head.uuid.is_some()); + assert_eq!(stream.head.uuid.unwrap(), HEADER_UUID); +} + +#[test] +fn read_box_header_truncated_uuid() { + const HEADER_UUID: [u8; 16] = [ + 0x85, 0xc0, 0xb6, 0x87, 0x82, 0x0f, 0x11, 0xe0, 0x81, 0x11, 0xf4, 0xce, 0x46, 0x2b, 0x6a, + 0x48, + ]; + + let mut stream = make_uuid_box(BoxSize::UncheckedShort(23), &HEADER_UUID, |s| s); + let mut iter = super::BoxIter::new(&mut stream); + let stream = iter.next_box().unwrap().unwrap(); + assert_eq!(stream.head.name, BoxType::UuidBox); + assert_eq!(stream.head.size, 23); + assert!(stream.head.uuid.is_none()); +} + +#[test] +fn read_ftyp() { + let mut stream = make_box(BoxSize::Short(24), b"ftyp", |s| { + s.append_bytes(b"mp42") + .B32(0) // minor version + .append_bytes(b"isom") + .append_bytes(b"mp42") + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + assert_eq!(stream.head.name, BoxType::FileTypeBox); + assert_eq!(stream.head.size, 24); + let parsed = super::read_ftyp(&mut stream).unwrap(); + assert_eq!(parsed.major_brand, b"mp42"); // mp42 + assert_eq!(parsed.minor_version, 0); + assert_eq!(parsed.compatible_brands.len(), 2); + assert_eq!(parsed.compatible_brands[0], b"isom"); // isom + assert_eq!(parsed.compatible_brands[1], b"mp42"); // mp42 +} + +#[test] +fn read_truncated_ftyp() { + // We declare a 24 byte box, but only write 20 bytes. + let mut stream = make_box(BoxSize::UncheckedShort(24), b"ftyp", |s| { + s.append_bytes(b"mp42") + .B32(0) // minor version + .append_bytes(b"isom") + }); + match read_mp4(&mut stream) { + Err(Error::UnexpectedEOF) => (), + Ok(_) => panic!("expected an error result"), + _ => panic!("expected a different error result"), + } +} + +#[test] +fn read_ftyp_case() { + // Brands in BMFF are represented as a u32, so it would seem clear that + // 0x6d703432 ("mp42") is not equal to 0x4d503432 ("MP42"), but some + // demuxers treat these as case-insensitive strings, e.g. street.mp4's + // major brand is "MP42". I haven't seen case-insensitive + // compatible_brands (which we also test here), but it doesn't seem + // unlikely given the major_brand behaviour. + let mut stream = make_box(BoxSize::Auto, b"ftyp", |s| { + s.append_bytes(b"MP42") + .B32(0) // minor version + .append_bytes(b"ISOM") + .append_bytes(b"MP42") + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + assert_eq!(stream.head.name, BoxType::FileTypeBox); + assert_eq!(stream.head.size, 24); + let parsed = super::read_ftyp(&mut stream).unwrap(); + assert_eq!(parsed.major_brand, b"MP42"); + assert_eq!(parsed.minor_version, 0); + assert_eq!(parsed.compatible_brands.len(), 2); + assert_eq!(parsed.compatible_brands[0], b"ISOM"); // ISOM + assert_eq!(parsed.compatible_brands[1], b"MP42"); // MP42 +} + +#[test] +fn read_elst_v0() { + let mut stream = make_fullbox(BoxSize::Short(28), b"elst", 0, |s| { + s.B32(1) // list count + // first entry + .B32(1234) // duration + .B32(5678) // time + .B16(12) // rate integer + .B16(34) // rate fraction + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + assert_eq!(stream.head.name, BoxType::EditListBox); + assert_eq!(stream.head.size, 28); + let parsed = super::read_elst(&mut stream).unwrap(); + assert_eq!(parsed.edits.len(), 1); + assert_eq!(parsed.edits[0].segment_duration, 1234); + assert_eq!(parsed.edits[0].media_time, 5678); + assert_eq!(parsed.edits[0].media_rate_integer, 12); + assert_eq!(parsed.edits[0].media_rate_fraction, 34); +} + +#[test] +fn read_elst_v1() { + let mut stream = make_fullbox(BoxSize::Short(56), b"elst", 1, |s| { + s.B32(2) // list count + // first entry + .B64(1234) // duration + .B64(5678) // time + .B16(12) // rate integer + .B16(34) // rate fraction + // second entry + .B64(1234) // duration + .B64(5678) // time + .B16(12) // rate integer + .B16(34) // rate fraction + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + assert_eq!(stream.head.name, BoxType::EditListBox); + assert_eq!(stream.head.size, 56); + let parsed = super::read_elst(&mut stream).unwrap(); + assert_eq!(parsed.edits.len(), 2); + assert_eq!(parsed.edits[1].segment_duration, 1234); + assert_eq!(parsed.edits[1].media_time, 5678); + assert_eq!(parsed.edits[1].media_rate_integer, 12); + assert_eq!(parsed.edits[1].media_rate_fraction, 34); +} + +#[test] +fn read_mdhd_v0() { + let mut stream = make_fullbox(BoxSize::Short(32), b"mdhd", 0, |s| { + s.B32(0) + .B32(0) + .B32(1234) // timescale + .B32(5678) // duration + .B32(0) + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + assert_eq!(stream.head.name, BoxType::MediaHeaderBox); + assert_eq!(stream.head.size, 32); + let parsed = super::read_mdhd(&mut stream).unwrap(); + assert_eq!(parsed.timescale, 1234); + assert_eq!(parsed.duration, 5678); +} + +#[test] +fn read_mdhd_v1() { + let mut stream = make_fullbox(BoxSize::Short(44), b"mdhd", 1, |s| { + s.B64(0) + .B64(0) + .B32(1234) // timescale + .B64(5678) // duration + .B32(0) + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + assert_eq!(stream.head.name, BoxType::MediaHeaderBox); + assert_eq!(stream.head.size, 44); + let parsed = super::read_mdhd(&mut stream).unwrap(); + assert_eq!(parsed.timescale, 1234); + assert_eq!(parsed.duration, 5678); +} + +#[test] +fn read_mdhd_unknown_duration() { + let mut stream = make_fullbox(BoxSize::Short(32), b"mdhd", 0, |s| { + s.B32(0) + .B32(0) + .B32(1234) // timescale + .B32(::std::u32::MAX) // duration + .B32(0) + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + assert_eq!(stream.head.name, BoxType::MediaHeaderBox); + assert_eq!(stream.head.size, 32); + let parsed = super::read_mdhd(&mut stream).unwrap(); + assert_eq!(parsed.timescale, 1234); + assert_eq!(parsed.duration, ::std::u64::MAX); +} + +#[test] +fn read_mdhd_invalid_timescale() { + let mut stream = make_fullbox(BoxSize::Short(44), b"mdhd", 1, |s| { + s.B64(0) + .B64(0) + .B32(0) // timescale + .B64(5678) // duration + .B32(0) + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + assert_eq!(stream.head.name, BoxType::MediaHeaderBox); + assert_eq!(stream.head.size, 44); + let r = super::parse_mdhd(&mut stream, &mut super::Track::new(0)); + assert!(r.is_err()); +} + +#[test] +fn read_mvhd_v0() { + let mut stream = make_fullbox(BoxSize::Short(108), b"mvhd", 0, |s| { + s.B32(0).B32(0).B32(1234).B32(5678).append_repeated(0, 80) + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + assert_eq!(stream.head.name, BoxType::MovieHeaderBox); + assert_eq!(stream.head.size, 108); + let parsed = super::read_mvhd(&mut stream).unwrap(); + assert_eq!(parsed.timescale, 1234); + assert_eq!(parsed.duration, 5678); +} + +#[test] +fn read_mvhd_v1() { + let mut stream = make_fullbox(BoxSize::Short(120), b"mvhd", 1, |s| { + s.B64(0).B64(0).B32(1234).B64(5678).append_repeated(0, 80) + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + assert_eq!(stream.head.name, BoxType::MovieHeaderBox); + assert_eq!(stream.head.size, 120); + let parsed = super::read_mvhd(&mut stream).unwrap(); + assert_eq!(parsed.timescale, 1234); + assert_eq!(parsed.duration, 5678); +} + +#[test] +fn read_mvhd_invalid_timescale() { + let mut stream = make_fullbox(BoxSize::Short(120), b"mvhd", 1, |s| { + s.B64(0).B64(0).B32(0).B64(5678).append_repeated(0, 80) + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + assert_eq!(stream.head.name, BoxType::MovieHeaderBox); + assert_eq!(stream.head.size, 120); + let r = super::parse_mvhd(&mut stream); + assert!(r.is_err()); +} + +#[test] +fn read_mvhd_unknown_duration() { + let mut stream = make_fullbox(BoxSize::Short(108), b"mvhd", 0, |s| { + s.B32(0) + .B32(0) + .B32(1234) + .B32(::std::u32::MAX) + .append_repeated(0, 80) + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + assert_eq!(stream.head.name, BoxType::MovieHeaderBox); + assert_eq!(stream.head.size, 108); + let parsed = super::read_mvhd(&mut stream).unwrap(); + assert_eq!(parsed.timescale, 1234); + assert_eq!(parsed.duration, ::std::u64::MAX); +} + +#[test] +fn read_vpcc_version_0() { + let data_length = 12u16; + let mut stream = make_fullbox(BoxSize::Auto, b"vpcC", 0, |s| { + s.B8(2) + .B8(0) + .B8(0x82) + .B8(0) + .B16(data_length) + .append_repeated(42, data_length as usize) + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + assert_eq!(stream.head.name, BoxType::VPCodecConfigurationBox); + let r = super::read_vpcc(&mut stream); + assert!(r.is_ok()); +} + +// TODO: it'd be better to find a real sample here. +#[test] +#[allow(clippy::unusual_byte_groupings)] // Allow odd grouping for test readability. +fn read_vpcc_version_1() { + let data_length = 12u16; + let mut stream = make_fullbox(BoxSize::Auto, b"vpcC", 1, |s| { + s.B8(2) // profile + .B8(0) // level + .B8(0b1000_011_0) // bitdepth (4 bits), chroma (3 bits), video full range (1 bit) + .B8(1) // color primaries + .B8(1) // transfer characteristics + .B8(1) // matrix + .B16(data_length) + .append_repeated(42, data_length as usize) + }); + + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + assert_eq!(stream.head.name, BoxType::VPCodecConfigurationBox); + let r = super::read_vpcc(&mut stream); + match r { + Ok(vpcc) => { + assert_eq!(vpcc.bit_depth, 8); + assert_eq!(vpcc.chroma_subsampling, 3); + assert!(!vpcc.video_full_range_flag); + assert_eq!(vpcc.matrix_coefficients.unwrap(), 1); + } + _ => panic!("vpcc parsing error"), + } +} + +#[test] +fn read_hdlr() { + let mut stream = make_fullbox(BoxSize::Short(45), b"hdlr", 0, |s| { + s.B32(0) + .append_bytes(b"vide") + .B32(0) + .B32(0) + .B32(0) + .append_bytes(b"VideoHandler") + .B8(0) // null-terminate string + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + assert_eq!(stream.head.name, BoxType::HandlerBox); + assert_eq!(stream.head.size, 45); + let parsed = super::read_hdlr(&mut stream, ParseStrictness::Normal).unwrap(); + assert_eq!(parsed.handler_type, b"vide"); +} + +#[test] +fn read_hdlr_multiple_nul_in_name() { + let mut stream = make_fullbox(BoxSize::Short(45), b"hdlr", 0, |s| { + s.B32(0) + .append_bytes(b"vide") + .B32(0) + .B32(0) + .B32(0) + .append_bytes(b"Vide\0Handler") + .B8(0) // null-terminate string + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + assert_eq!(stream.head.name, BoxType::HandlerBox); + assert_eq!(stream.head.size, 45); + assert!(super::read_hdlr(&mut stream, ParseStrictness::Strict).is_err()); +} + +#[test] +fn read_hdlr_short_name() { + let mut stream = make_fullbox(BoxSize::Short(33), b"hdlr", 0, |s| { + s.B32(0).append_bytes(b"vide").B32(0).B32(0).B32(0).B8(0) // null-terminate string + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + assert_eq!(stream.head.name, BoxType::HandlerBox); + assert_eq!(stream.head.size, 33); + let parsed = super::read_hdlr(&mut stream, ParseStrictness::Normal).unwrap(); + assert_eq!(parsed.handler_type, b"vide"); +} + +#[test] +fn read_hdlr_unsupported_version() { + let mut stream = make_fullbox(BoxSize::Short(32), b"hdlr", 1, |s| { + s.B32(0).append_bytes(b"vide").B32(0).B32(0).B32(0) + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + assert_eq!(stream.head.name, BoxType::HandlerBox); + assert_eq!(stream.head.size, 32); + match super::read_hdlr(&mut stream, ParseStrictness::Normal) { + Err(Error::Unsupported(msg)) => assert_eq!("hdlr version", msg), + result => { + eprintln!("{:?}", result); + panic!("expected Error::Unsupported") + } + } +} + +#[test] +fn read_hdlr_invalid_pre_defined_field() { + let mut stream = make_fullbox(BoxSize::Short(32), b"hdlr", 0, |s| { + s.B32(1).append_bytes(b"vide").B32(0).B32(0).B32(0) + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + assert_eq!(stream.head.name, BoxType::HandlerBox); + assert_eq!(stream.head.size, 32); + match super::read_hdlr(&mut stream, ParseStrictness::Strict) { + Err(Error::InvalidData(msg)) => assert_eq!( + "The HandlerBox 'pre_defined' field shall be 0 \ + per ISOBMFF (ISO 14496-12:2020) § 8.4.3.2", + msg + ), + result => { + eprintln!("{:?}", result); + panic!("expected Error::InvalidData") + } + } +} + +#[test] +fn read_hdlr_invalid_reserved_field() { + let mut stream = make_fullbox(BoxSize::Short(32), b"hdlr", 0, |s| { + s.B32(0).append_bytes(b"vide").B32(0).B32(1).B32(0) + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + assert_eq!(stream.head.name, BoxType::HandlerBox); + assert_eq!(stream.head.size, 32); + match super::read_hdlr(&mut stream, ParseStrictness::Strict) { + Err(Error::InvalidData(msg)) => assert_eq!( + "The HandlerBox 'reserved' fields shall be 0 \ + per ISOBMFF (ISO 14496-12:2020) § 8.4.3.2", + msg + ), + result => { + eprintln!("{:?}", result); + panic!("expected Error::InvalidData") + } + } +} + +#[test] +fn read_hdlr_zero_length_name() { + let mut stream = make_fullbox(BoxSize::Short(32), b"hdlr", 0, |s| { + s.B32(0).append_bytes(b"vide").B32(0).B32(0).B32(0) + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + assert_eq!(stream.head.name, BoxType::HandlerBox); + assert_eq!(stream.head.size, 32); + match super::read_hdlr(&mut stream, ParseStrictness::Normal) { + Err(Error::InvalidData(msg)) => assert_eq!( + "The HandlerBox 'name' field shall be null-terminated \ + per ISOBMFF (ISO 14496-12:2020) § 8.4.3.2", + msg + ), + result => { + eprintln!("{:?}", result); + panic!("expected Error::InvalidData") + } + } +} + +#[test] +fn read_hdlr_zero_length_name_permissive() { + let mut stream = make_fullbox(BoxSize::Short(32), b"hdlr", 0, |s| { + s.B32(0).append_bytes(b"vide").B32(0).B32(0).B32(0) + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + assert_eq!(stream.head.name, BoxType::HandlerBox); + assert_eq!(stream.head.size, 32); + let parsed = super::read_hdlr(&mut stream, ParseStrictness::Permissive).unwrap(); + assert_eq!(parsed.handler_type, b"vide"); +} + +fn flac_streaminfo() -> Vec { + vec![ + 0x10, 0x00, 0x10, 0x00, 0x00, 0x0a, 0x11, 0x00, 0x38, 0x32, 0x0a, 0xc4, 0x42, 0xf0, 0x00, + 0xc9, 0xdf, 0xae, 0xb5, 0x66, 0xfc, 0x02, 0x15, 0xa3, 0xb1, 0x54, 0x61, 0x47, 0x0f, 0xfb, + 0x05, 0x00, 0x33, 0xad, + ] +} + +#[test] +fn read_flac() { + let mut stream = make_box(BoxSize::Auto, b"fLaC", |s| { + s.append_repeated(0, 6) // reserved + .B16(1) // data reference index + .B32(0) // reserved + .B32(0) // reserved + .B16(2) // channel count + .B16(16) // bits per sample + .B16(0) // pre_defined + .B16(0) // reserved + .B32(44100 << 16) // Sample rate + .append_bytes( + &make_dfla( + FlacBlockType::StreamInfo, + true, + &flac_streaminfo(), + FlacBlockLength::Correct, + ) + .into_inner(), + ) + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + let r = super::read_audio_sample_entry(&mut stream); + assert!(r.is_ok()); +} + +#[derive(Clone, Copy)] +enum FlacBlockType { + StreamInfo = 0, + _Padding = 1, + _Application = 2, + _Seektable = 3, + _Comment = 4, + _Cuesheet = 5, + _Picture = 6, + _Reserved, + _Invalid = 127, +} + +enum FlacBlockLength { + Correct, + Incorrect(usize), +} + +fn make_dfla( + block_type: FlacBlockType, + last: bool, + data: &[u8], + data_length: FlacBlockLength, +) -> Cursor> { + assert!(data.len() < 1 << 24); + make_fullbox(BoxSize::Auto, b"dfLa", 0, |s| { + let flag = if last { 1 } else { 0 }; + let size = match data_length { + FlacBlockLength::Correct => (data.len() as u32) & 0x00ff_ffff, + FlacBlockLength::Incorrect(size) => { + assert!(size < 1 << 24); + (size as u32) & 0x00ff_ffff + } + }; + let block_type = (block_type as u32) & 0x7f; + s.B32(flag << 31 | block_type << 24 | size) + .append_bytes(data) + }) +} + +#[test] +fn read_dfla() { + let mut stream = make_dfla( + FlacBlockType::StreamInfo, + true, + &flac_streaminfo(), + FlacBlockLength::Correct, + ); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + assert_eq!(stream.head.name, BoxType::FLACSpecificBox); + let dfla = super::read_dfla(&mut stream).unwrap(); + assert_eq!(dfla.version, 0); +} + +#[test] +fn long_flac_metadata() { + let streaminfo = flac_streaminfo(); + let mut stream = make_dfla( + FlacBlockType::StreamInfo, + true, + &streaminfo, + FlacBlockLength::Incorrect(streaminfo.len() + 4), + ); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + assert_eq!(stream.head.name, BoxType::FLACSpecificBox); + let r = super::read_dfla(&mut stream); + assert!(r.is_err()); +} + +#[test] +fn read_opus() { + let mut stream = make_box(BoxSize::Auto, b"Opus", |s| { + s.append_repeated(0, 6) + .B16(1) // data reference index + .B32(0) + .B32(0) + .B16(2) // channel count + .B16(16) // bits per sample + .B16(0) + .B16(0) + .B32(48000 << 16) // Sample rate is always 48 kHz for Opus. + .append_bytes(&make_dops().into_inner()) + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + let r = super::read_audio_sample_entry(&mut stream); + assert!(r.is_ok()); +} + +fn make_dops() -> Cursor> { + make_box(BoxSize::Auto, b"dOps", |s| { + s.B8(0) // version + .B8(2) // channel count + .B16(348) // pre-skip + .B32(44100) // original sample rate + .B16(0) // gain + .B8(0) // channel mapping + }) +} + +#[test] +fn read_dops() { + let mut stream = make_dops(); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + assert_eq!(stream.head.name, BoxType::OpusSpecificBox); + let r = super::read_dops(&mut stream); + assert!(r.is_ok()); +} + +#[test] +fn serialize_opus_header() { + let opus = super::OpusSpecificBox { + version: 0, + output_channel_count: 1, + pre_skip: 342, + input_sample_rate: 24000, + output_gain: 0, + channel_mapping_family: 0, + channel_mapping_table: None, + }; + let mut v = Vec::::new(); + super::serialize_opus_header(&opus, &mut v).unwrap(); + assert_eq!(v.len(), 19); + assert_eq!( + v, + vec![ + 0x4f, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64, 0x01, 0x01, 0x56, 0x01, 0xc0, 0x5d, + 0x00, 0x00, 0x00, 0x00, 0x00, + ] + ); + let opus = super::OpusSpecificBox { + version: 0, + output_channel_count: 6, + pre_skip: 152, + input_sample_rate: 48000, + output_gain: 0, + channel_mapping_family: 1, + channel_mapping_table: Some(super::ChannelMappingTable { + stream_count: 4, + coupled_count: 2, + channel_mapping: vec![0, 4, 1, 2, 3, 5].into(), + }), + }; + let mut v = Vec::::new(); + super::serialize_opus_header(&opus, &mut v).unwrap(); + assert_eq!(v.len(), 27); + assert_eq!( + v, + vec![ + 0x4f, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64, 0x01, 0x06, 0x98, 0x00, 0x80, 0xbb, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x04, 0x02, 0x00, 0x04, 0x01, 0x02, 0x03, 0x05, + ] + ); +} + +#[test] +fn read_alac() { + let mut stream = make_box(BoxSize::Auto, b"alac", |s| { + s.append_repeated(0, 6) // reserved + .B16(1) // data reference index + .B32(0) // reserved + .B32(0) // reserved + .B16(2) // channel count + .B16(16) // bits per sample + .B16(0) // pre_defined + .B16(0) // reserved + .B32(44100 << 16) // Sample rate + .append_bytes( + &make_fullbox(BoxSize::Auto, b"alac", 0, |s| s.append_bytes(&[0xfa; 24])) + .into_inner(), + ) + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + let r = super::read_audio_sample_entry(&mut stream); + assert!(r.is_ok()); +} + +#[test] +fn esds_limit() { + let mut stream = make_box(BoxSize::Auto, b"mp4a", |s| { + s.append_repeated(0, 6) + .B16(1) + .B32(0) + .B32(0) + .B16(2) + .B16(16) + .B16(0) + .B16(0) + .B32(48000 << 16) + .B32(8) + .append_bytes(b"esds") + .append_repeated(0, 4) + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + match super::read_audio_sample_entry(&mut stream) { + Err(Error::UnexpectedEOF) => (), + Ok(_) => panic!("expected an error result"), + _ => panic!("expected a different error result"), + } +} + +#[test] +fn read_elst_zero_entries() { + let mut stream = make_fullbox(BoxSize::Auto, b"elst", 0, |s| s.B32(0).B16(12).B16(34)); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + match super::read_elst(&mut stream) { + Ok(elst) => assert_eq!(elst.edits.len(), 0), + _ => panic!("expected no error"), + } +} + +fn make_elst() -> Cursor> { + make_fullbox(BoxSize::Auto, b"elst", 1, |s| { + s.B32(1) + // first entry + .B64(1234) // duration + .B64(0xffff_ffff_ffff_ffff) // time + .B16(12) // rate integer + .B16(34) // rate fraction + }) +} + +#[test] +fn read_edts_bogus() { + // First edit list entry has a media_time of -1, so we expect a second + // edit list entry to be present to provide a valid media_time. + // Bogus edts are ignored. + let mut stream = make_box(BoxSize::Auto, b"edts", |s| { + s.append_bytes(&make_elst().into_inner()) + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + let mut track = super::Track::new(0); + match super::read_edts(&mut stream, &mut track) { + Ok(_) => { + assert_eq!(track.media_time, None); + assert_eq!(track.empty_duration, None); + } + _ => panic!("expected no error"), + } +} + +#[test] +fn skip_padding_in_boxes() { + // Padding data could be added in the end of these boxes. Parser needs to skip + // them instead of returning error. + let box_names = vec![b"stts", b"stsc", b"stsz", b"stco", b"co64", b"stss"]; + + for name in box_names { + let mut stream = make_fullbox(BoxSize::Auto, name, 1, |s| { + s.append_repeated(0, 100) // add padding data + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + match name { + b"stts" => { + super::read_stts(&mut stream).expect("fail to skip padding: stts"); + } + b"stsc" => { + super::read_stsc(&mut stream).expect("fail to skip padding: stsc"); + } + b"stsz" => { + super::read_stsz(&mut stream).expect("fail to skip padding: stsz"); + } + b"stco" => { + super::read_stco(&mut stream).expect("fail to skip padding: stco"); + } + b"co64" => { + super::read_co64(&mut stream).expect("fail to skip padding: co64"); + } + b"stss" => { + super::read_stss(&mut stream).expect("fail to skip padding: stss"); + } + _ => (), + } + } +} + +#[test] +fn skip_padding_in_stsd() { + // Padding data could be added in the end of stsd boxes. Parser needs to skip + // them instead of returning error. + let avc = make_box(BoxSize::Auto, b"avc1", |s| { + s.append_repeated(0, 6) + .B16(1) + .append_repeated(0, 16) + .B16(320) + .B16(240) + .append_repeated(0, 14) + .append_repeated(0, 32) + .append_repeated(0, 4) + .B32(0xffff_ffff) + .append_bytes(b"avcC") + .append_repeated(0, 100) + }) + .into_inner(); + let mut stream = make_fullbox(BoxSize::Auto, b"stsd", 0, |s| { + s.B32(1) + .append_bytes(avc.as_slice()) + .append_repeated(0, 100) // add padding data + }); + + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + super::read_stsd(&mut stream, &mut super::Track::new(0)).expect("fail to skip padding: stsd"); +} + +#[test] +fn read_qt_wave_atom() { + let esds = make_fullbox(BoxSize::Auto, b"esds", 0, |s| { + s.B8(0x03) // elementary stream descriptor tag + .B8(0x12) // esds length + .append_repeated(0, 2) + .B8(0x00) // flags + .B8(0x04) // decoder config descriptor tag + .B8(0x0d) // dcds length + .B8(0x6b) // mp3 + .append_repeated(0, 12) + }) + .into_inner(); + let chan = make_box(BoxSize::Auto, b"chan", |s| { + s.append_repeated(0, 10) // we don't care its data. + }) + .into_inner(); + let wave = make_box(BoxSize::Auto, b"wave", |s| s.append_bytes(esds.as_slice())).into_inner(); + let mut stream = make_box(BoxSize::Auto, b"mp4a", |s| { + s.append_repeated(0, 6) + .B16(1) // data_reference_count + .B16(1) // verion: qt -> 1 + .append_repeated(0, 6) + .B16(2) + .B16(16) + .append_repeated(0, 4) + .B32(48000 << 16) + .append_repeated(0, 16) + .append_bytes(wave.as_slice()) + .append_bytes(chan.as_slice()) + }); + + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + let sample_entry = + super::read_audio_sample_entry(&mut stream).expect("fail to read qt wave atom"); + match sample_entry { + super::SampleEntry::Audio(sample_entry) => { + assert_eq!(sample_entry.codec_type, super::CodecType::MP3) + } + _ => panic!("fail to read audio sample enctry"), + } +} + +#[test] +fn read_descriptor_80() { + let aac_esds = vec![ + 0x03, 0x80, 0x80, 0x80, 0x22, 0x00, 0x02, 0x00, 0x04, 0x80, 0x80, 0x80, 0x17, 0x40, 0x15, + 0x00, 0x00, 0x00, 0x00, 0x03, 0x22, 0xBC, 0x00, 0x01, 0xF5, 0x83, 0x05, 0x80, 0x80, 0x80, + 0x02, 0x11, 0x90, 0x06, 0x80, 0x80, 0x80, 0x01, 0x02, + ]; + let aac_dc_descriptor = &aac_esds[31..33]; + let mut stream = make_box(BoxSize::Auto, b"esds", |s| { + s.B32(0) // reserved + .append_bytes(aac_esds.as_slice()) + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + + let es = super::read_esds(&mut stream).unwrap(); + + assert_eq!(es.audio_codec, super::CodecType::AAC); + assert_eq!(es.audio_object_type, Some(2)); + assert_eq!(es.extended_audio_object_type, None); + assert_eq!(es.audio_sample_rate, Some(48000)); + assert_eq!(es.audio_channel_count, Some(2)); + assert_eq!(es.codec_esds, aac_esds); + assert_eq!(es.decoder_specific_data, aac_dc_descriptor); +} + +#[test] +fn read_esds() { + let aac_esds = vec![ + 0x03, 0x24, 0x00, 0x00, 0x00, 0x04, 0x1c, 0x40, 0x15, 0x00, 0x12, 0x00, 0x00, 0x01, 0xf4, + 0x00, 0x00, 0x01, 0xf4, 0x00, 0x05, 0x0d, 0x13, 0x00, 0x05, 0x88, 0x05, 0x00, 0x48, 0x21, + 0x10, 0x00, 0x56, 0xe5, 0x98, 0x06, 0x01, 0x02, + ]; + let aac_dc_descriptor = &aac_esds[22..35]; + + let mut stream = make_box(BoxSize::Auto, b"esds", |s| { + s.B32(0) // reserved + .append_bytes(aac_esds.as_slice()) + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + + let es = super::read_esds(&mut stream).unwrap(); + + assert_eq!(es.audio_codec, super::CodecType::AAC); + assert_eq!(es.audio_object_type, Some(2)); + assert_eq!(es.extended_audio_object_type, None); + assert_eq!(es.audio_sample_rate, Some(24000)); + assert_eq!(es.audio_channel_count, Some(6)); + assert_eq!(es.codec_esds, aac_esds); + assert_eq!(es.decoder_specific_data, aac_dc_descriptor); +} + +#[test] +fn read_esds_aac_type5() { + let aac_esds = vec![ + 0x03, 0x80, 0x80, 0x80, 0x2F, 0x00, 0x00, 0x00, 0x04, 0x80, 0x80, 0x80, 0x21, 0x40, 0x15, + 0x00, 0x15, 0x00, 0x00, 0x03, 0xED, 0xAA, 0x00, 0x03, 0x6B, 0x00, 0x05, 0x80, 0x80, 0x80, + 0x0F, 0x2B, 0x01, 0x88, 0x02, 0xC4, 0x04, 0x90, 0x2C, 0x10, 0x8C, 0x80, 0x00, 0x00, 0xED, + 0x40, 0x06, 0x80, 0x80, 0x80, 0x01, 0x02, + ]; + + let aac_dc_descriptor = &aac_esds[31..46]; + + let mut stream = make_box(BoxSize::Auto, b"esds", |s| { + s.B32(0) // reserved + .append_bytes(aac_esds.as_slice()) + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + + let es = super::read_esds(&mut stream).unwrap(); + + assert_eq!(es.audio_codec, super::CodecType::AAC); + assert_eq!(es.audio_object_type, Some(2)); + assert_eq!(es.extended_audio_object_type, Some(5)); + assert_eq!(es.audio_sample_rate, Some(24000)); + assert_eq!(es.audio_channel_count, Some(8)); + assert_eq!(es.codec_esds, aac_esds); + assert_eq!(es.decoder_specific_data, aac_dc_descriptor); +} + +#[test] +fn read_stsd_mp4v() { + let mp4v = vec![ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xd0, 0x01, 0xe0, 0x00, 0x48, + 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, + 0x18, 0xff, 0xff, 0x00, 0x00, 0x00, 0x4c, 0x65, 0x73, 0x64, 0x73, 0x00, 0x00, 0x00, 0x00, + 0x03, 0x3e, 0x00, 0x00, 0x1f, 0x04, 0x36, 0x20, 0x11, 0x01, 0x77, 0x00, 0x00, 0x03, 0xe8, + 0x00, 0x00, 0x03, 0xe8, 0x00, 0x05, 0x27, 0x00, 0x00, 0x01, 0xb0, 0x05, 0x00, 0x00, 0x01, + 0xb5, 0x0e, 0xcf, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x20, 0x00, 0x86, 0xe0, 0x00, + 0x2e, 0xa6, 0x60, 0x16, 0xf4, 0x01, 0xf4, 0x24, 0xc8, 0x01, 0xe5, 0x16, 0x84, 0x3c, 0x14, + 0x63, 0x06, 0x01, 0x02, + ]; + #[cfg(not(feature = "mp4v"))] + let esds_specific_data = &mp4v[90..]; + #[cfg(feature = "mp4v")] + let esds_specific_data = &mp4v[112..151]; + println!("esds_specific_data {:?}", esds_specific_data); + + let mut stream = make_box(BoxSize::Auto, b"mp4v", |s| s.append_bytes(mp4v.as_slice())); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + + let sample_entry = super::read_video_sample_entry(&mut stream).unwrap(); + + match sample_entry { + super::SampleEntry::Video(v) => { + assert_eq!(v.codec_type, super::CodecType::MP4V); + assert_eq!(v.width, 720); + assert_eq!(v.height, 480); + match v.codec_specific { + super::VideoCodecSpecific::ESDSConfig(esds_data) => { + assert_eq!(esds_data.as_slice(), esds_specific_data); + } + _ => panic!("it should be ESDSConfig!"), + } + } + _ => panic!("it should be a video sample entry!"), + } +} + +#[test] +fn read_esds_one_byte_extension_descriptor() { + let esds = vec![ + 0x00, 0x03, 0x80, 0x1b, 0x00, 0x00, 0x00, 0x04, 0x80, 0x12, 0x40, 0x15, 0x00, 0x06, 0x00, + 0x00, 0x01, 0xfe, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x05, 0x80, 0x02, 0x11, 0x90, 0x06, 0x01, + 0x02, + ]; + + let mut stream = make_box(BoxSize::Auto, b"esds", |s| { + s.B32(0) // reserved + .append_bytes(esds.as_slice()) + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + + let es = super::read_esds(&mut stream).unwrap(); + + assert_eq!(es.audio_codec, super::CodecType::AAC); + assert_eq!(es.audio_object_type, Some(2)); + assert_eq!(es.extended_audio_object_type, None); + assert_eq!(es.audio_sample_rate, Some(48000)); + assert_eq!(es.audio_channel_count, Some(2)); +} + +#[test] +fn read_esds_byte_extension_descriptor() { + let mut stream = make_box(BoxSize::Auto, b"esds", |s| { + s.B32(0) // reserved + .B16(0x0003) + .B16(0x8181) // extension byte length 0x81 + .append_repeated(0, 0x81) + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + + match super::read_esds(&mut stream) { + Ok(_) => (), + _ => panic!("fail to parse descriptor extension byte length"), + } +} + +#[test] +fn read_f4v_stsd() { + let mut stream = make_box(BoxSize::Auto, b".mp3", |s| { + s.append_repeated(0, 6) + .B16(1) + .B16(0) + .append_repeated(0, 6) + .B16(2) + .B16(16) + .append_repeated(0, 4) + .B32(48000 << 16) + }); + + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + let sample_entry = + super::read_audio_sample_entry(&mut stream).expect("failed to read f4v stsd atom"); + match sample_entry { + super::SampleEntry::Audio(sample_entry) => { + assert_eq!(sample_entry.codec_type, super::CodecType::MP3) + } + _ => panic!("fail to read audio sample enctry"), + } +} + +#[test] +fn unknown_video_sample_entry() { + let unknown_codec = make_box(BoxSize::Auto, b"yyyy", |s| s.append_repeated(0, 16)).into_inner(); + let mut stream = make_box(BoxSize::Auto, b"xxxx", |s| { + s.append_repeated(0, 6) + .B16(1) + .append_repeated(0, 16) + .B16(0) + .B16(0) + .append_repeated(0, 14) + .append_repeated(0, 32) + .append_repeated(0, 4) + .append_bytes(unknown_codec.as_slice()) + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + match super::read_video_sample_entry(&mut stream) { + Ok(super::SampleEntry::Unknown) => (), + _ => panic!("expected a different error result"), + } +} + +#[test] +fn unknown_audio_sample_entry() { + let unknown_codec = make_box(BoxSize::Auto, b"yyyy", |s| s.append_repeated(0, 16)).into_inner(); + let mut stream = make_box(BoxSize::Auto, b"xxxx", |s| { + s.append_repeated(0, 6) + .B16(1) + .B32(0) + .B32(0) + .B16(2) + .B16(16) + .B16(0) + .B16(0) + .B32(48000 << 16) + .append_bytes(unknown_codec.as_slice()) + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + match super::read_audio_sample_entry(&mut stream) { + Ok(super::SampleEntry::Unknown) => (), + _ => panic!("expected a different error result"), + } +} + +#[test] +fn read_esds_invalid_descriptor() { + // tag 0x06, 0xff, 0x7f is incorrect. + let esds = vec![ + 0x03, 0x80, 0x80, 0x80, 0x22, 0x00, 0x00, 0x00, 0x04, 0x80, 0x80, 0x80, 0x14, 0x40, 0x01, + 0x00, 0x04, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x05, 0x80, 0x80, 0x80, + 0x02, 0xe8, 0x35, 0x06, 0xff, 0x7f, 0x00, 0x00, + ]; + + let mut stream = make_box(BoxSize::Auto, b"esds", |s| { + s.B32(0) // reserved + .append_bytes(esds.as_slice()) + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + + match super::read_esds(&mut stream) { + Err(Error::InvalidData(s)) => assert_eq!(s, "Invalid descriptor."), + _ => panic!("unexpected result with invalid descriptor"), + } +} + +#[test] +fn read_esds_redundant_descriptor() { + // the '2' at the end is redundant data. + let esds = vec![ + 3, 25, 0, 1, 0, 4, 19, 64, 21, 0, 0, 0, 0, 0, 0, 0, 0, 1, 119, 0, 5, 2, 18, 16, 6, 1, 2, + ]; + + let mut stream = make_box(BoxSize::Auto, b"esds", |s| { + s.B32(0) // reserved + .append_bytes(esds.as_slice()) + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + + match super::read_esds(&mut stream) { + Ok(esds) => assert_eq!(esds.audio_codec, super::CodecType::AAC), + _ => panic!("unexpected result with invalid descriptor"), + } +} + +#[test] +fn read_stsd_lpcm() { + // Extract from sample converted by ffmpeg. + // "ffmpeg -i ./gizmo-short.mp4 -acodec pcm_s16le -ar 96000 -vcodec copy -f mov gizmo-short.mov" + let lpcm = vec![ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x03, 0x00, 0x10, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x48, 0x40, 0xf7, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x7f, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x18, 0x63, 0x68, 0x61, 0x6e, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x64, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + + let mut stream = make_box(BoxSize::Auto, b"lpcm", |s| s.append_bytes(lpcm.as_slice())); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + + let sample_entry = super::read_audio_sample_entry(&mut stream).unwrap(); + + match sample_entry { + #[allow(clippy::float_cmp)] // The float comparison below is valid and intended. + super::SampleEntry::Audio(a) => { + assert_eq!(a.codec_type, super::CodecType::LPCM); + assert_eq!(a.samplerate, 96000.0); + assert_eq!(a.channelcount, 1); + match a.codec_specific { + super::AudioCodecSpecific::LPCM => (), + _ => panic!("it should be LPCM!"), + } + } + _ => panic!("it should be a audio sample entry!"), + } +} + +#[test] +fn read_to_end_() { + let mut src = b"1234567890".take(5); + let buf = src.read_into_try_vec().unwrap(); + assert_eq!(buf.len(), 5); + assert_eq!(buf, b"12345".as_ref()); +} + +#[test] +fn read_to_end_oom() { + let mut src = b"1234567890".take(std::usize::MAX.try_into().expect("usize < u64")); + assert!(src.read_into_try_vec().is_err()); +} diff --git a/src/unstable.rs b/src/unstable.rs new file mode 100644 index 0000000..eeb16f8 --- /dev/null +++ b/src/unstable.rs @@ -0,0 +1,546 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +use num_traits::{CheckedAdd, CheckedSub, PrimInt, Zero}; +use std::ops::{Add, Neg, Sub}; + +use super::*; + +/// A zero-overhead wrapper around integer types for the sake of always +/// requiring checked arithmetic +#[repr(transparent)] +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct CheckedInteger(pub T); + +impl From for CheckedInteger { + fn from(i: T) -> Self { + Self(i) + } +} + +// Orphan rules prevent a more general implementation, but this suffices +impl From> for i64 { + fn from(checked: CheckedInteger) -> i64 { + checked.0 + } +} + +impl> Add for CheckedInteger +where + T: CheckedAdd, +{ + type Output = Option; + + fn add(self, other: U) -> Self::Output { + self.0.checked_add(&other.into()).map(Into::into) + } +} + +impl> Sub for CheckedInteger +where + T: CheckedSub, +{ + type Output = Option; + + fn sub(self, other: U) -> Self::Output { + self.0.checked_sub(&other.into()).map(Into::into) + } +} + +/// Implement subtraction of checked `u64`s returning i64 +// This is necessary for handling Mp4parseTrackInfo::media_time gracefully +impl Sub for CheckedInteger { + type Output = Option>; + + fn sub(self, other: Self) -> Self::Output { + if self >= other { + self.0 + .checked_sub(other.0) + .and_then(|u| i64::try_from(u).ok()) + .map(CheckedInteger) + } else { + other + .0 + .checked_sub(self.0) + .and_then(|u| i64::try_from(u).ok()) + .map(i64::neg) + .map(CheckedInteger) + } + } +} + +#[test] +fn u64_subtraction_returning_i64() { + // self > other + assert_eq!( + CheckedInteger(2u64) - CheckedInteger(1u64), + Some(CheckedInteger(1i64)) + ); + + // self == other + assert_eq!( + CheckedInteger(1u64) - CheckedInteger(1u64), + Some(CheckedInteger(0i64)) + ); + + // difference too large to store in i64 + assert_eq!(CheckedInteger(u64::MAX) - CheckedInteger(1u64), None); + + // self < other + assert_eq!( + CheckedInteger(1u64) - CheckedInteger(2u64), + Some(CheckedInteger(-1i64)) + ); + + // difference not representable due to overflow + assert_eq!(CheckedInteger(1u64) - CheckedInteger(u64::MAX), None); +} + +impl PartialEq for CheckedInteger { + fn eq(&self, other: &T) -> bool { + self.0 == *other + } +} + +/// Provides the following information about a sample in the source file: +/// sample data offset (start and end), composition time in microseconds +/// (start and end) and whether it is a sync sample +#[repr(C)] +#[derive(Default, Debug, PartialEq)] +pub struct Indice { + /// The byte offset in the file where the indexed sample begins. + pub start_offset: CheckedInteger, + /// The byte offset in the file where the indexed sample ends. This is + /// equivalent to `start_offset` + the length in bytes of the indexed + /// sample. Typically this will be the `start_offset` of the next sample + /// in the file. + pub end_offset: CheckedInteger, + /// The time in microseconds when the indexed sample should be displayed. + /// Analogous to the concept of presentation time stamp (pts). + pub start_composition: CheckedInteger, + /// The time in microseconds when the indexed sample should stop being + /// displayed. Typically this would be the `start_composition` time of the + /// next sample if samples were ordered by composition time. + pub end_composition: CheckedInteger, + /// The time in microseconds that the indexed sample should be decoded at. + /// Analogous to the concept of decode time stamp (dts). + pub start_decode: CheckedInteger, + /// Set if the indexed sample is a sync sample. The meaning of sync is + /// somewhat codec specific, but essentially amounts to if the sample is a + /// key frame. + pub sync: bool, +} + +/// Create a vector of `Indice`s with the information about track samples. +/// It uses `stsc`, `stco`, `stsz` and `stts` boxes to construct a list of +/// every sample in the file and provides offsets which can be used to read +/// raw sample data from the file. +#[allow(clippy::reversed_empty_ranges)] +pub fn create_sample_table( + track: &Track, + track_offset_time: CheckedInteger, +) -> Option> { + let timescale = match track.timescale { + Some(ref t) => TrackTimeScale::(t.0 as i64, t.1), + _ => return None, + }; + + let (stsc, stco, stsz, stts) = match (&track.stsc, &track.stco, &track.stsz, &track.stts) { + (&Some(ref a), &Some(ref b), &Some(ref c), &Some(ref d)) => (a, b, c, d), + _ => return None, + }; + + // According to spec, no sync table means every sample is sync sample. + let has_sync_table = matches!(track.stss, Some(_)); + + let mut sample_size_iter = stsz.sample_sizes.iter(); + + // Get 'stsc' iterator for (chunk_id, chunk_sample_count) and calculate the sample + // offset address. + + // With large numbers of samples, the cost of many allocations dominates, + // so it's worth iterating twice to allocate sample_table just once. + let total_sample_count = sample_to_chunk_iter(&stsc.samples, &stco.offsets) + .map(|(_, sample_counts)| sample_counts.to_usize()) + .try_fold(0usize, usize::checked_add)?; + let mut sample_table = TryVec::with_capacity(total_sample_count).ok()?; + + for i in sample_to_chunk_iter(&stsc.samples, &stco.offsets) { + let chunk_id = i.0 as usize; + let sample_counts = i.1; + let mut cur_position = match stco.offsets.get(chunk_id) { + Some(&i) => i.into(), + _ => return None, + }; + for _ in 0..sample_counts { + let start_offset = cur_position; + let end_offset = match (stsz.sample_size, sample_size_iter.next()) { + (_, Some(t)) => (start_offset + *t)?, + (t, _) if t > 0 => (start_offset + t)?, + _ => 0.into(), + }; + if end_offset == 0 { + return None; + } + cur_position = end_offset; + + sample_table + .push(Indice { + start_offset, + end_offset, + sync: !has_sync_table, + ..Default::default() + }) + .ok()?; + } + } + + // Mark the sync sample in sample_table according to 'stss'. + if let Some(ref v) = track.stss { + for iter in &v.samples { + match iter + .checked_sub(&1) + .and_then(|idx| sample_table.get_mut(idx as usize)) + { + Some(elem) => elem.sync = true, + _ => return None, + } + } + } + + let ctts_iter = track.ctts.as_ref().map(|v| v.samples.as_slice().iter()); + + let mut ctts_offset_iter = TimeOffsetIterator { + cur_sample_range: (0..0), + cur_offset: 0, + ctts_iter, + track_id: track.id, + }; + + let mut stts_iter = TimeToSampleIterator { + cur_sample_count: (0..0), + cur_sample_delta: 0, + stts_iter: stts.samples.as_slice().iter(), + track_id: track.id, + }; + + // sum_delta is the sum of stts_iter delta. + // According to spec: + // decode time => DT(n) = DT(n-1) + STTS(n) + // composition time => CT(n) = DT(n) + CTTS(n) + // Note: + // composition time needs to add the track offset time from 'elst' table. + let mut sum_delta = TrackScaledTime::(0, track.id); + for sample in sample_table.as_mut_slice() { + let decode_time = sum_delta; + sum_delta = (sum_delta + stts_iter.next_delta())?; + + // ctts_offset is the current sample offset time. + let ctts_offset = ctts_offset_iter.next_offset_time(); + + let start_composition = track_time_to_us((decode_time + ctts_offset)?, timescale)?.0; + + let end_composition = track_time_to_us((sum_delta + ctts_offset)?, timescale)?.0; + + let start_decode = track_time_to_us(decode_time, timescale)?.0; + + sample.start_composition = (track_offset_time + start_composition)?; + sample.end_composition = (track_offset_time + end_composition)?; + sample.start_decode = start_decode.into(); + } + + // Correct composition end time due to 'ctts' causes composition time re-ordering. + // + // Composition end time is not in specification. However, gecko needs it, so we need to + // calculate to correct the composition end time. + if !sample_table.is_empty() { + // Create an index table refers to sample_table and sorted by start_composisiton time. + let mut sort_table = TryVec::with_capacity(sample_table.len()).ok()?; + + for i in 0..sample_table.len() { + sort_table.push(i).ok()?; + } + + sort_table.sort_by_key(|i| match sample_table.get(*i) { + Some(v) => v.start_composition, + _ => 0.into(), + }); + + for indices in sort_table.windows(2) { + if let [current_index, peek_index] = *indices { + let next_start_composition_time = sample_table[peek_index].start_composition; + let sample = &mut sample_table[current_index]; + sample.end_composition = next_start_composition_time; + } + } + } + + Some(sample_table) +} + +// Convert a 'ctts' compact table to full table by iterator, +// (sample_with_the_same_offset_count, offset) => (offset), (offset), (offset) ... +// +// For example: +// (2, 10), (4, 9) into (10, 10, 9, 9, 9, 9) by calling next_offset_time(). +struct TimeOffsetIterator<'a> { + cur_sample_range: std::ops::Range, + cur_offset: i64, + ctts_iter: Option>, + track_id: usize, +} + +impl<'a> Iterator for TimeOffsetIterator<'a> { + type Item = i64; + + #[allow(clippy::reversed_empty_ranges)] + fn next(&mut self) -> Option { + let has_sample = self.cur_sample_range.next().or_else(|| { + // At end of current TimeOffset, find the next TimeOffset. + let iter = match self.ctts_iter { + Some(ref mut v) => v, + _ => return None, + }; + let offset_version; + self.cur_sample_range = match iter.next() { + Some(v) => { + offset_version = v.time_offset; + 0..v.sample_count + } + _ => { + offset_version = TimeOffsetVersion::Version0(0); + 0..0 + } + }; + + self.cur_offset = match offset_version { + TimeOffsetVersion::Version0(i) => i64::from(i), + TimeOffsetVersion::Version1(i) => i64::from(i), + }; + + self.cur_sample_range.next() + }); + + has_sample.and(Some(self.cur_offset)) + } +} + +impl<'a> TimeOffsetIterator<'a> { + fn next_offset_time(&mut self) -> TrackScaledTime { + match self.next() { + Some(v) => TrackScaledTime::(v as i64, self.track_id), + _ => TrackScaledTime::(0, self.track_id), + } + } +} + +// Convert 'stts' compact table to full table by iterator, +// (sample_count_with_the_same_time, time) => (time, time, time) ... repeats +// sample_count_with_the_same_time. +// +// For example: +// (2, 3000), (1, 2999) to (3000, 3000, 2999). +struct TimeToSampleIterator<'a> { + cur_sample_count: std::ops::Range, + cur_sample_delta: u32, + stts_iter: std::slice::Iter<'a, Sample>, + track_id: usize, +} + +impl<'a> Iterator for TimeToSampleIterator<'a> { + type Item = u32; + + #[allow(clippy::reversed_empty_ranges)] + fn next(&mut self) -> Option { + let has_sample = self.cur_sample_count.next().or_else(|| { + self.cur_sample_count = match self.stts_iter.next() { + Some(v) => { + self.cur_sample_delta = v.sample_delta; + 0..v.sample_count + } + _ => 0..0, + }; + + self.cur_sample_count.next() + }); + + has_sample.and(Some(self.cur_sample_delta)) + } +} + +impl<'a> TimeToSampleIterator<'a> { + fn next_delta(&mut self) -> TrackScaledTime { + match self.next() { + Some(v) => TrackScaledTime::(i64::from(v), self.track_id), + _ => TrackScaledTime::(0, self.track_id), + } + } +} + +// Convert 'stco' compact table to full table by iterator. +// (start_chunk_num, sample_number) => (start_chunk_num, sample_number), +// (start_chunk_num + 1, sample_number), +// (start_chunk_num + 2, sample_number), +// ... +// (next start_chunk_num, next sample_number), +// ... +// +// For example: +// (1, 5), (5, 10), (9, 2) => (1, 5), (2, 5), (3, 5), (4, 5), (5, 10), (6, 10), +// (7, 10), (8, 10), (9, 2) +fn sample_to_chunk_iter<'a>( + stsc_samples: &'a TryVec, + stco_offsets: &'a TryVec, +) -> SampleToChunkIterator<'a> { + SampleToChunkIterator { + chunks: (0..0), + sample_count: 0, + stsc_peek_iter: stsc_samples.as_slice().iter().peekable(), + remain_chunk_count: stco_offsets + .len() + .try_into() + .expect("stco.entry_count is u32"), + } +} + +struct SampleToChunkIterator<'a> { + chunks: std::ops::Range, + sample_count: u32, + stsc_peek_iter: std::iter::Peekable>, + remain_chunk_count: u32, // total chunk number from 'stco'. +} + +impl<'a> Iterator for SampleToChunkIterator<'a> { + type Item = (u32, u32); + + fn next(&mut self) -> Option<(u32, u32)> { + let has_chunk = self.chunks.next().or_else(|| { + self.chunks = self.locate(); + self.remain_chunk_count + .checked_sub( + self.chunks + .len() + .try_into() + .expect("len() of a Range must fit in u32"), + ) + .and_then(|res| { + self.remain_chunk_count = res; + self.chunks.next() + }) + }); + + has_chunk.map(|id| (id, self.sample_count)) + } +} + +impl<'a> SampleToChunkIterator<'a> { + #[allow(clippy::reversed_empty_ranges)] + fn locate(&mut self) -> std::ops::Range { + loop { + return match (self.stsc_peek_iter.next(), self.stsc_peek_iter.peek()) { + (Some(next), Some(peek)) if next.first_chunk == peek.first_chunk => { + // Invalid entry, skip it and will continue searching at + // next loop iteration. + continue; + } + (Some(next), Some(peek)) if next.first_chunk > 0 && peek.first_chunk > 0 => { + self.sample_count = next.samples_per_chunk; + (next.first_chunk - 1)..(peek.first_chunk - 1) + } + (Some(next), None) if next.first_chunk > 0 => { + self.sample_count = next.samples_per_chunk; + // Total chunk number in 'stsc' could be different to 'stco', + // there could be more chunks at the last 'stsc' record. + match next.first_chunk.checked_add(self.remain_chunk_count) { + Some(r) => (next.first_chunk - 1)..r - 1, + _ => 0..0, + } + } + _ => 0..0, + }; + } + } +} + +/// Calculate numerator * scale / denominator, if possible. +/// +/// Applying the associativity of integer arithmetic, we divide first +/// and add the remainder after multiplying each term separately +/// to preserve precision while leaving more headroom. That is, +/// (n * s) / d is split into floor(n / d) * s + (n % d) * s / d. +/// +/// Return None on overflow or if the denominator is zero. +fn rational_scale(numerator: T, denominator: T, scale2: S) -> Option +where + T: PrimInt + Zero, + S: PrimInt, +{ + if denominator.is_zero() { + return None; + } + + let integer = numerator / denominator; + let remainder = numerator % denominator; + num_traits::cast(scale2).and_then(|s| match integer.checked_mul(&s) { + Some(integer) => remainder + .checked_mul(&s) + .and_then(|remainder| (remainder / denominator).checked_add(&integer)), + None => None, + }) +} + +#[derive(Debug, PartialEq)] +pub struct Microseconds(pub T); + +/// Convert `time` in media's global (mvhd) timescale to microseconds, +/// using provided `MediaTimeScale` +pub fn media_time_to_us(time: MediaScaledTime, scale: MediaTimeScale) -> Option> { + let microseconds_per_second = 1_000_000; + rational_scale(time.0, scale.0, microseconds_per_second).map(Microseconds) +} + +/// Convert `time` in track's local (mdhd) timescale to microseconds, +/// using provided `TrackTimeScale` +pub fn track_time_to_us( + time: TrackScaledTime, + scale: TrackTimeScale, +) -> Option> +where + T: PrimInt + Zero, +{ + assert_eq!(time.1, scale.1); + let microseconds_per_second = 1_000_000; + rational_scale(time.0, scale.0, microseconds_per_second).map(Microseconds) +} + +#[test] +fn rational_scale_overflow() { + assert_eq!(rational_scale::(17, 3, 1000), Some(5666)); + let large = 0x4000_0000_0000_0000; + assert_eq!(rational_scale::(large, 2, 2), Some(large)); + assert_eq!(rational_scale::(large, 4, 4), Some(large)); + assert_eq!(rational_scale::(large, 2, 8), None); + assert_eq!(rational_scale::(large, 8, 4), Some(large / 2)); + assert_eq!(rational_scale::(large + 1, 4, 4), Some(large + 1)); + assert_eq!(rational_scale::(large, 40, 1000), None); +} + +#[test] +fn media_time_overflow() { + let scale = MediaTimeScale(90000); + let duration = MediaScaledTime(9_007_199_254_710_000); + assert_eq!( + media_time_to_us(duration, scale), + Some(Microseconds(100_079_991_719_000_000u64)) + ); +} + +#[test] +fn track_time_overflow() { + let scale = TrackTimeScale(44100u64, 0); + let duration = TrackScaledTime(4_413_527_634_807_900u64, 0); + assert_eq!( + track_time_to_us(duration, scale), + Some(Microseconds(100_079_991_719_000_000u64)) + ); +} diff --git a/tests/1x1-black-alpha-50pct-premultiplied.avif b/tests/1x1-black-alpha-50pct-premultiplied.avif new file mode 100644 index 0000000000000000000000000000000000000000..40652dfd4934ff04dd524d3dd9941abca0aad336 GIT binary patch literal 1106 zcmZ`&O=uHA6#lZ=BvoTuY|ttN_fSg@u{7zyHn$`Nd#F-s1Qjov%_bSx%?`Vp+TcY| z&|^WwpNoH2&w3R+dr|NQ#G5y7q8BOgn+YL3v@guOZ@%|4llR^Nu$pmC+FPOtiqgJl z7JOkhFO&;qz%+d~wv~-C-)?x}00L4t@enKRiTW_jxD>IkZ3*sq;RTL5tpWxV!)2~e zpX7OP%2OZZstD21FVU7$Jh;k5Mc7-5g=*jh`T-*g&B{{tWZPDr^CENy8Gvyax;|qQ zI@`G2-S+egm@SzXGL+OQA)SD6IT1-$C7{r**Q6BNLJ`Tau`#Nwy;Z2*wuoTVoWKiZ z81OD*&2#+7Tm<}19B!VkSXZuIvob%C!W2$m4l}kB$@0d=x{BOi{(jas;?r4`Yk!{u z*&_`%VucgxjjN6f*_VX6YyCzn6@E;dbJ});_%(4pUK6nl+{~^CK;%1)#6L*0(??+ty5%BgNF#S#1 zp{?&xo3XSss1tEg5wE@gwVQ0u2jIz5V2(31ah--KHB%kO+vb!SNZ%s0-&peV8jPiG z_+ThmAIdUEoJ|9|VW;Fvx23mA$q|k$MZG9?{isAA;R!qJmF(e1b6QA$_+Wmbsf2OI uOu;bInfr&BZNYEY@i81Wr;&bGOim)pLuMK29m^8M>~XrRarM?e8vg;AXujJ3 literal 0 HcmV?d00001 diff --git a/tests/amr_nb_1f.3gp b/tests/amr_nb_1f.3gp new file mode 100644 index 0000000000000000000000000000000000000000..5ef216c63638100690b9e6b1eaaaf1584916ee12 GIT binary patch literal 701 zcmZuuKTpC?5I@S|pa~}UXJ{e=gM%gRf3B5^S>F>Wq}YoVc!mcB<5ob)634OpH0 z1`fu=uc3p{(Qlx4ZC{|oORx9)bMJa>0l;+CB&_$seER;A<8+Lh=G{%{u=>1|4`&kv$;u zMh%i@7UvrEwDP=`h}zM{0&i(OtyKq=BxpLmRGGymRw!%f&V{Bx&n8|GxcKf{Ez+mg zuXG34?V}$hh;A|%4>jg`MI6uVA^J|z^C`TcOL|F)+|Vkfx`~m)J_j$fJdXTIh99Ns z_=L}-lKN+GPea;5FTgwv14;~%RRLC!dfM%aOSu0{25(B0d;#v^L!47elZJPxa%f%G zGI_VD1(nPvts7(-_w7)gj8WNC>nOV+duwm8ht1fkS(dwp7DXrYj}Id?>NPsYK|dZH II7b%z0k@-DtpET3 literal 0 HcmV?d00001 diff --git a/tests/amr_wb_1f.3gp b/tests/amr_wb_1f.3gp new file mode 100644 index 0000000000000000000000000000000000000000..a49f39438d449f024966e89e673642f1df5fb666 GIT binary patch literal 713 zcmZQzV30{GsVp!~FEC+XU}DHD&d&v6BM6^?BdsVkm4Si5H8&-(#6^(Jztl0ukiBFJK3L6{6o0*&tg6Nk_$sVODJK&DJ;PH_oT3Z#P{hyx&QX4K40$xH?M1{chI)bf-h;5uhvsgKkDjP7#;|B#QG(^I&Wc9Sr2BcqHbfa*s)Ydq E0DSOi!TYc0uY^>nP!-qnV9D5Xy^zO`jnemk_eIm0*#E6oFWL5fuSHX zxdg@r(K(q(Fk|=%GD~v7a*RMyE;A=T8N_p8U| F9RR)FMqvN| literal 0 HcmV?d00001 diff --git a/tests/bad-ipma-flags.avif b/tests/bad-ipma-flags.avif new file mode 100644 index 0000000000000000000000000000000000000000..40012f2114aa4c236f5716a2d4305c41a8b801cf GIT binary patch literal 437 zcmZQzU{FXasVqn=%S>Yc0uY^>nP!-qnV9D5Xy^zO`j(qok_eIm0*#E6oFWL5fuSHX zxdg@r(K(q(Fk|=%GD~v7a*RMyE;A=T8N_p8U|XmxCL3JG`HI*?-0DoM�Z_ZxHx@E*PZ6f!7-)(EMU$M)zuRFD$yY6VC H>MTnD14MRY literal 0 HcmV?d00001 diff --git a/tests/bad-ipma-version.avif b/tests/bad-ipma-version.avif new file mode 100644 index 0000000000000000000000000000000000000000..e8442c166c789e2937b7c2d17d7872231f32a679 GIT binary patch literal 435 zcmZQzU{FXasVqn=%S>Yc0uY^>nP!-qnV9D5Xy^zO`jVSkk_eIm0*#E6oFWL5fuSHX zxdg@r(K(q(Fk|=%GD~v7a*RMyE;A=T8N_p8U|% z>j{48yz9%sjk_J*PrdBF;&#q6x5hVTuVmdaVwyIQ`@iqDHQBG&<=WSs+Rt5gv{7}I FB>ZQ5a zo8I*1SMWm=dhr8z7sPu{f;hgJZO9tEdh27~%-)^~}AqRta2G+p2ISTzD3a6?O(Vxy##`l0-TLL%|4K>!rK^J3R^nNvMtS`kaXfz(&Dc(dsg{N$G17 zNo|Z3>%Wo91YhAxrNad9Tcy?FKy1vZ>K*i9wo+HeaUKoabvMb%nqd>xk5bp}A(QheO Bsr3K= literal 0 HcmV?d00001 diff --git a/tests/clap-basic-1_3x3-to-1x1.avif b/tests/clap-basic-1_3x3-to-1x1.avif new file mode 100644 index 0000000000000000000000000000000000000000..2d59986a28c18db25329ca4a2f4e9e4d7bdabb91 GIT binary patch literal 355 zcmX|6u}Z{15S`pb38HeGa>xl15G17$3rmk!*j#5}yY3}8uzOi@ONgB!c2@ce_d%9^ zjD>I3i*K2E^WH2oyF`>${>iD9D*}WqSGi?X&1d~sx2 zdY>bAJa~974tg2oL*myC$Mj6G`#!h?$3U{-%r1O0VrHJ&Hw-a literal 0 HcmV?d00001 diff --git a/tests/clusterfuzz-testcase-minimized-mp4-6093954524250112 b/tests/clusterfuzz-testcase-minimized-mp4-6093954524250112 new file mode 100644 index 0000000000000000000000000000000000000000..fb6335f5631b86d0c6da8c39cab9378fe30dbb38 GIT binary patch literal 56 zcmZ=}$ef#-pI^qvz`#&al$fo?z|N7Il9|ZBz#g8PnU|JZP*PT0l9a<>$dQ{|Qc|3m JpAV8u1OUuW5a<8^ literal 0 HcmV?d00001 diff --git a/tests/corrupt/bug-1655846.avif b/tests/corrupt/bug-1655846.avif new file mode 100644 index 0000000000000000000000000000000000000000..31c7e424548d7583d81f0ae7fd5ef7ffa4fc3116 GIT binary patch literal 256 zcmZQzU{FXasVqn=%S>Yc0uY^>nP!-qnV9D5Xy^zO`jnemk_eIm0*#E6oFWL5fuSHX zxdg@r(K(q(Fk|=%=9c6_k#4-az=lq;}u$5Vv1w{owaqrB6pF literal 0 HcmV?d00001 diff --git a/tests/corrupt/bug-1661347.avif b/tests/corrupt/bug-1661347.avif new file mode 100644 index 0000000000000000000000000000000000000000..11b539230bb4a33aecf3d63d3cb2bee33453d014 GIT binary patch literal 8468 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 F0|3huM&SSe literal 0 HcmV?d00001 diff --git a/tests/corrupt/hdlr-not-pict.avif b/tests/corrupt/hdlr-not-pict.avif new file mode 100644 index 0000000000000000000000000000000000000000..34137ce3c95c9710e6c0dde1e45d440f9ae61f94 GIT binary patch literal 334 zcmZQzU{FXasVqn=%S>Yc0uY^>nP!-qnV9D5Xy^zO`jnemk_eIm0*#E6oFWL5fuSHj zzW~Mt(K(q(Fk|=%GD~v7a*RMyE;A=T8N_p8U|pVvCKD{^=sDL)A3x(EUfZ#C;fiP_y5kqte4Ms F008UAM(+Rs literal 0 HcmV?d00001 diff --git a/tests/corrupt/imir-before-clap.avif b/tests/corrupt/imir-before-clap.avif new file mode 100644 index 0000000000000000000000000000000000000000..d02ef72424fcb07afdeb1f06086d7f193849491d GIT binary patch literal 345 zcmYk1y-or_5XWZ^VonM|VGI&tV&#R(v$P_yGQ5G^ZQsFX3C9JUbrH1x4sN)dDzYCv|j8(B9$T>{kK*h#j}#Y=o>R@96_8#ybuKhy1Yb6}G=(B~$9{LF|torlrK=6{_GXpCA<IIh+pP(+;pkIZXfn literal 0 HcmV?d00001 diff --git a/tests/corrupt/invalid-avif-colr-multiple-nclx.avif b/tests/corrupt/invalid-avif-colr-multiple-nclx.avif new file mode 100644 index 0000000000000000000000000000000000000000..7d2ea0dc5255c56f28ebee9c1c60c748ae6a314f GIT binary patch literal 315 zcmXv|&1%9x5T2x%2GJt66}*&K@ZR*)tJp&i?GyC0tXVpcHA7s2f(J`|0H4jr@zihB ze9O%D$1Y1mnF|{+4LbsqJ-ghqabL51hUnyrV6+1!KZ~-yNFreyTHW%UaPVgnVLx$M zS#|G35sq$(p21_xlzHhi1e6|Oi}>nH^OCFFveW|pX0a9cfmc#!0bC!Xf$*9&xJlC# zG!V1WJ8j=HO+Nq4kPzM5s;rl`T%+w%vO_dx@kToUzf7X|Z{O*_O#H%xA$4!xC^=*q OzxGB*KQNomWBdNm?rU#F{W#;|nKX2cgdAk6Z;qi{)C^!1Dy1d*_ zZ%(}8sr`BvR>9l6%Ju60!pcZ@o?QI$qtV=1*qFxslTvN7uiEpQhGR&Nx~cVuPu}#U z;t_GyqeBrtR@{mBx#HD`x5AiKvraLeiJ8&mZMqvFa;tmv&K@4uRWaG7X%yH}>sr3a zYOTWg;)VPmK>vyW94FrN~+#RMYpd;Uv745s=1p$ z5pJz#ef1Us8Su=|MWeE?oM6gMZN9p$4&Lz~g`JL~Z{ZOq$k?jJ-dQNhfJ QF;Py>l6slOu<=I!126xUEC2ui literal 0 HcmV?d00001 diff --git a/tests/corrupt/invalid-avif-colr-multiple-rICC.avif b/tests/corrupt/invalid-avif-colr-multiple-rICC.avif new file mode 100644 index 0000000000000000000000000000000000000000..e8ced19127f3487d795c364ed5da006f18204a77 GIT binary patch literal 864 zcmb`FJ%|%Q6vzL&F_#d628pK!e$3%Vm4qbX87oO5oPh{uh@jU^HoM6{b~9|2c zfyQYe97~C0${nJmsZUmXm@g3O4~29k`CCF7eG<}QAWaoRHc5z-6l+2Wk$5~#E+zSZ z-ub(q{f7w^pqZXSkHTeWcos=Qfrr?D~yW7W;%maJ*wU%8h|6udLM7 zn^UiNX20GARq!^ea=p60v^v(ECl|l`s5f?&HfC}Eq(s{ssP_D(;TY1TZfZT^lQ%u7 zcubsj=}^Rv6?Z~@uDBKQRuI!{(0#Pe#LVdOHr)xI3LqSN(QnYv}Nnq%ZLte7uQN!9zX==Rm{%gt}iwEiYg z`bl4SJkweafM<7sBkyP0sSV)3I`H(hGNOw&!y&neQ)EzK#(EXYaK zE%J1B)&q%$hVU}5?=`bb+z-Scy0n6ufsy4aBLf4_WCn(*lYKLU0|kz!n=j*3%$T?( z>ceAEdq2+;ZficOc(zD&1Wf0+b^COhdF2NA{=2GX=1bq!9_+rC_4Qe-ZH2%dF&0*S zrMd?8ey@w(y07=#y|Uz6fR(|S{D0Q(v+qSSD74u>*m9o7LHJ@)TasZ`%zCZRh9jBY z-!+ez-F`je-GoNtv+e)i7^LqLo!C;>HM37`(rTsYNv4MVg=~LilD8=y-2Y0`r?hg$ z#X3*NN%Ds8ih9Z=ugi!Ooq254!^S!PNlquH;zYmYi|16;ys4h{ZR#&uC9Nl2&t|N< zz2f@B&r^y&3#^*oC zQ6G}K_YC||3Xq~fIOvcj9?ZSJOg@8b4;(*435eKq z{9u%`(Q?(_x+cFYc=9X9nhzEGzuz~H&rf4)h&VXmGUH|jU$^$iS%+7Yu3UGOG3dCA z@q!6AV)kcu3o@s>G1T9PJIAE1#`;%dv#qn5c*;7JtSrXII=?)R#dtT~zshc4dNt6E zU6j{z@A(}MB=QaNDl|l9ocERBpJaFTCf8&qt=VUfS$_R_p*p;-$v_Z+!b7 zov`kR{?eBp7bfhjb9i+_ce2hdx1AoVM0uxA+3UZ`TwmSXe6!)TJ*5q|%J)1hSX26r z(Pk%a!Hc&M)*IP2)3;Rc!Bm?VoH8ziLHt z=P{kQ!+3*laYnXn*EabF56T(N$F7-`dgta(?hl&&ny=PgEs>gRx!k+??}XD=60}rW zLO&a?h;xzO7PoRgFs-q3*vgbDbKy;U*1%GWK*5)noKrzm+DlGI2=LHespFw@+9#QT zNnq~mnQ1d-&z$k>b;b)0kzZ41M@>4xz}O|JY3UkZ=IoeO#K|o6K>LKwN!HC0jS+gr z>h_XH3K>62DlY2ra)=V@2sJkTd`Uv`!7A6rJ0WTs`Ww$25IC~tq>hFb!=;NeSx^3S zh*H_SLt!f8Le3UeE+cItN2W6}GBOEuNrjA)Z2XgEE-YTm6wzN$ROc5MSLDYU&^Ens zE#s4EXPWM~?3mb-)8lhicg-4w`_GoCOye*t*cffF!8ah_gvld8=B_G+!lma`)xe>6 z&g^3M6=1;*3abEbMkWzvLsc61hN1C literal 0 HcmV?d00001 diff --git a/tests/corrupt/ipma-duplicate-item_id.avif b/tests/corrupt/ipma-duplicate-item_id.avif new file mode 100644 index 0000000000000000000000000000000000000000..ed47ebfe6fc711cc6c053a25df68ef44c86e179e GIT binary patch literal 49032 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 z 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 literal 0 HcmV?d00001 diff --git a/tests/corrupt/no-alpha-av1C.avif b/tests/corrupt/no-alpha-av1C.avif new file mode 100644 index 0000000000000000000000000000000000000000..6d674ebbbfcc1377bd9778c9816ac110628780c7 GIT binary patch literal 437 zcmYLFO-sW-5S>kG4H8Qc5iK6#UBvWMa!b>L2mKLCnzRGiW=Yb}Tmw3i772y5*b@f`;?Zu+CrNFeXwPlQmjA#NBpXR7Xl=Ic)|$;KXuc zdZDo1b5W+ERzwa-1*yJ)H_%7*0X#>pn0L9fAjF$NWEXw}ue+RZWabdSw zI%iyE{WHKPdVeVTQ)TE6+sXo8*`_7%l53j*CB&DUT0XE8T+j0WzHY2`W}ALlMk%K@s=K<$OdX+z3)WQ^>EV92Y)yYrY{h@L%6TWpRihK=2ea1A f1)YtqY5X;Jr{qJNE@=E21U&bz(DH=ro`0P`O}tAE literal 0 HcmV?d00001 diff --git a/tests/corrupt/no-av1C.avif b/tests/corrupt/no-av1C.avif new file mode 100644 index 0000000000000000000000000000000000000000..f46fcb65236222198d8661371699ea2486326256 GIT binary patch literal 281 zcmXv|I|{-;5S>KOAR<9ju#s5U8!HQ&fQ5x#z-~#hIN<7_OE9$;@N8Bd$2V%;vh#k- zvP6_vUyG^O5}@qZrjCu>XK9A$?MiR717^qKbp1&p;oLX6;l8kUUlU+X`E<$EZHNLK zonN8}^ql!@H3XDKux0pnrdp&b4=h&!uUYa7c*r*?wE+GPQY?ID4O&qYfnqT?-D&zU ji!<5-_(m%TA5b$_7(bwHXGQI2lJK%OM!LnVcdzgR&`K#T literal 0 HcmV?d00001 diff --git a/tests/corrupt/no-hdlr.avif b/tests/corrupt/no-hdlr.avif new file mode 100644 index 0000000000000000000000000000000000000000..d75e8695895ce76340aa40a463214f2bd671fbe3 GIT binary patch literal 294 zcmZQzU{FXasVqn=%S>Yc0uY^>nP!-qnV9D5Xy^zOI+dGRk_eIm0=|OGl3Xy05lG5q z=Hw@XcrFYKj6et?85lkRu?Y}sWM<}p%>;`|0XeBmKw%IsvCP2GIX@@A2*l6IEGQ}f z(%zW`$@xH9B(u066(j)!DNrm>kXexl5@6=w-~duQiDib)jS4^(1A}mKeoj$da!v)% zWL^fg2B465WI4vYMKF(z1Wt(86D!{<7C2;fG kyeG;(micD0e$ASDI-YBpg;jp;q~A~Z{@+=c_44@+0N?^aF#rGn literal 0 HcmV?d00001 diff --git a/tests/corrupt/no-ispe.avif b/tests/corrupt/no-ispe.avif new file mode 100644 index 0000000000000000000000000000000000000000..69f97caeae50d8eb2bea423ccd4cc66bdaa6371a GIT binary patch literal 273 zcmXv{%?g4*5FQmuLL!2MF0mfEXLRT)b?8u+-e6Ie2Hae1kvvu}(4$39)3F)LZ<+c2 zU={#`vW<~7Dj+axRIx^>4oRF)^wPPN+!3NvR#ZO;U@*0v?s#a>>ee{4XIPXeyDfl2 zk2k=FW=bos{P(`?hkU2&ZHf~pudLRQwUrJA*DN-X8(4-du}Se literal 0 HcmV?d00001 diff --git a/tests/corrupt/no-pixi-for-alpha.avif b/tests/corrupt/no-pixi-for-alpha.avif new file mode 100644 index 0000000000000000000000000000000000000000..caf1b7fe60675d868eb8059bf8afce9b89907f2b GIT binary patch literal 522 zcmZutJ4*vW5T47+7-9%&p+O-af`Ww!7U8-iwkhoFWWC&(iMw~p-6bSop$OKJ!p=g3 z^rD627YO!NRz4yYmV#grXYUL)4$OWt^UZSsAaD6y$=YZEi9&%^K_Ihgj~3m ziX&NR)VUj)01^#bA%pAIA*a0BJS{)wDR8#Lzx{~k`rQs0Hq9;v4 zCG}s;O6;HtM3c!RL7KG-)uTLF05gWeUEAOt+GYw8hZOp+MOXr9RZ^k_s>*1&wl1W( z7d#c+Ktt+Y*Yi!`>6C*UncLOb_=rW6BDjW1>C0kTYc0uY^>nP!-qnV9D5Xy^zOdYGG9k_eIm0*#E6oFWL5fuSHX zxdg@r(K(q(Fk|=%GD~v7a*RMyE;A=T8N_p8U|_r>P0T=+O>RnJi4X%9r-kIk$C-@0Y!i%B s1sE8%1a5wt_eA-}GT&_0uUT_X$8#;Su*%P!^!q8_|2qq_UOwLe09I8*vj6}9 literal 0 HcmV?d00001 diff --git a/tests/hdlr-nonzero-reserved.avif b/tests/hdlr-nonzero-reserved.avif new file mode 100644 index 0000000000000000000000000000000000000000..e84ba63e2d1d748a7b7427b009d1f229c3874e60 GIT binary patch literal 294 zcmXwxO>V+45QWE~f(X$HAqrxH!lL_z66LH7!QTRd zIppmwQ#U6vI6BF^f&<(S=5MDVqjz^000h_#xDPl z|MSNG4JM!^uyAp4aOR??H@9@La5bVcwzs9Xv9z#rH#Gue*hu?0Qj%{ zGeG(ONUG%@QdwHsnf)*MFa0mZ`WKo4{ZI5?g$#_6wua`W|HW_)mM*sc!T)r=mJUu1 z|5(@3!Px$vM{qE7{xAM-j{iPLOIu5){{Y3(+2McV0RVs&{uS{r^-pZ+Vfh~bLqI_M za~OthjKYDSFaS_cm}a*4zd=A)NdL=QI@lWir~7CB3I+uP0uBlNS9rIriJ{B?uf+mI zBme+FCCW<$eLF$FFVCz}i&Q5~bP)>N*bErVuOWPYwJi0_XS_bO46j1Y%k_g57Q138 z(0}a}QSCmOvh%oD2TeI4q+K7*_t2y#8@nE$x$pKZd%zxyvht)tAh&}~tXFIgNO7I~ z#_~%+4Bm0j=q@63Qa%&ZijO1l?VRsj*nF=4nm7cVwWV{Fju&xwb>*|mPWk#)f4}h! zi=CKN=q8;t!91Y{L;M@X|2I46^=JYbNe*+TvyEV+y3}0$o!9&>1DM0`*i*HS=3Q50 zY>Ls$qQ}-g+hh3uGm>iCrvu8Cz663v61I*wQInjoW|Qhq%|MQxI&{tQxc?729#{x< z@+eJ~Sf1HNTVuP5!{iUN+6ZIOXsV+j-k~i1Y%{KGrEyXSpsMPBT}g z--Ix6F0^bdSIm)lXV}v!X7!PP(_82b6IZp_w@ENv)1Spf(O#Qzb2V0$_OO$wJGOyG zCC^^p%ykrlK}zOuQs7{<6(@Ct$dfl}g|obrdlw4M>OO$4nFYgENo^DCDFsvG{WivF zoww*g8FA6~@BO+_{l8COZjb46h-Y1*$_CGU6Dco}NYa0vjZldD+^HTen5)CX$6*Pn zCl_9)8*BX*A`Z9LEBRH;x@j>q#CwO=Z9(=wjaKkIk}U2Jl~yV-mC5B3JrSBL5v-F< zi|HNiRF=4+>{6UD+>iQJwk+@6LdRr-Hue&UR5fsVcG_S@#@_<={2hU*aFda4KMG*b zeIqYo3=?)jpe1I|R6kmU31i?u>Ov6oBZAR`&1okg?U$A3R4Z=|`XK_TQqb~ab>o!eC z!a^a|VVw?_Fix|6#<@gvlMr`{d8YeR zgkPQHocQpR%c@tqs2&h<+KiqP%qPq<5H+C7`oW;$81{a1zyv{Au{T=et=r)JK~X^3 zt7S~Q-DjA(dMw0`;t3L5J7{AE(gpYDxIOy2?OdloT@tsHy$qx5Y%_BF&}v_;`(DC{sG4G(KM0!6U5?>+SdC{9z_08UBsXih=WO(|@Fe7Pz>vRCiPl-dafa;XwrHqg6O24UNLpGD6&Hu7cJz@3 zb1>-RW8Z3!SU*v$NZtijc{*G;NJQx*j7TM>Le)nmx>{JGi+Y8!oOw*wc3*yD$9=)6 zWj9WupsjJ~DBy}wtRML-$(ek0Yh1q~fn*Ukz|E--rp!o=uJ%w(qLHx=X{8Re>Se+~ zCrTTzbWf(1I9X=YDLrrr;JWgb7|*mPPPfN5oi&11W2{btIIl&LwF_LX)8x`cDvBW= zg8~Nx$zzjMx(7q!;;!zm{-Ves0*AQIUzkQaP;FZk4?4q^VSdA)W0^kTuGKiTf_*xT zs;>?6;U7uXc3wY~33R^w9%%b>ogF%=;4KjVGrMbVrWnzSD7Hk%;9o+1f{DLgWpe{w zl)=oENR2N}_u@dTz%InWV-iISLVF9t#iL7TTrJ^Q$0RvhFFj|oI&mVRP`fe%hmbU= zK^%68`JJqy(`?=WV%WuyXeCH9=nZ2v1V>bd<}4k+TvG7No53<(f0Q%eCB??a-KR45 z+w+JRv}U{>NmAJY`7fi(6bi&Pec`l~TlMht#*|mC7~oV&z%K$&ag?qOZ53kKp<|6! zsRkuio;J$yLND>-iBj@e{EaYxxRh=VRtF=36SHVWr zm$B8G0<(zqW79U|wfZ)Z-I9`-Yq^J$SzUO=ldW-q>zBdxdMIOoZDa>rjz`DQ4Q#7F z;8kd-=f7a+x(l5ewS0)L%3#e3-E|D<>a(7nL*GL8aGd?-z!OpP;NA}5TrPQ7JLzS0 zJ?tCB=8vECTmAU8XYIcb{loCt>8TtX+@O-W{F>o=af|;nr5QuKFGlzCyP#_&2KC)? zXt^yn1zZQ0W5h^)*t7@lT_4B4ApDnG+c`jS06XY0$&f;ze37^y-ud(E(oi8qwLg)r z`W4hM_|+%(xfyLAnUkF;3^@1_xCJdW(dis*R#Xh0M3|Ya4K6d7xvhs?qI>rQ$q(B_ zuD8;Ai3`ev*%Yh&ZX{9ED{yqs$q|>F1Nclvqulw%qyA7x{OVUv%~vhceO8zVAgrCS zQb-CvgV+h2npbPUHzt58m4hbFTl_d4gcEvu~#ah6uF)CSC6vv zWx?d2i}VTIC(C|$fz)=Mk1ksSl_DJGA5;Ml#MaLes~aXd-!Yqzzst(TVnxfkKa+V< zg4w7z6bBq_T)PzVvAY^sN5l9om>(^yCqR*+QCF-|@0VYNLYlE~G9~6KMUrs!7poLw zcwYneIRt61-1ElipnzzAFHR!XihIF{dCKOkR=q@z{ng{r3Ia=3S*3zEh6Eg9q$PDY zJ9Oj2vjK&GkR$J7iGRyAuCsLaW#pMGRMSiunmla5(V#H=1YZUx2ci) z!m`c_VcPUmMD+oba7|85W%Cds0=6%hgADopSfMiYU)kdv{Zr&52k;LOkZMtDFs;X_^v z$oA_S0{K(lY|9o6tCfw1?Yk>S@1*+6wj|FK_jW9(DoAicabc5;97tBEzogHF+TKtQ~kp!`EAar zZvHX4>B6<9F;)xvng1ra(b8)f_Kjw}Og~!7+eO&EW;6b3NyqDBb!b$T7Is z7VF6Ykv#e>mI0@vSrgM_&5=U2RN$4n^vXvd36ct}m0bN)gRBwoYJ8qG^d8BV-~?j^ z3u?9^z21hr=bEJ9qLE3|xgtnlbC70y^a-XJYGG>gN;{E=A4ez?q0_8@?-+*#@s+B5 z1HmW5w9zdg)Uw>rMzz0X&vg~bC5X)T3QWNW66f^wgM>fvzVV=jt)XFe6+Ix`5ar&> zNB*sI3z5|2A-XJao+E)5kiCOuM*9Hx7xlQ=fj@$}u0`8JqF5`R(c{A=8ZBC8FwJJ~ z=oLtsXhjcA$|_4mAkxZpOFdt`n3@8um?-1+@SgZUXAK7kAB!VL*qX+H;^nc`EB zQ6j*F1ZSY49aSzE;k0TgVyaShmLC{g74Y#oz-TcWP&t`M)!}FU*F>9q`u^md)f;|j zIUBExhsxFKc;2k|?EBx~(Lb$`cTjU_mVsoj@999}W{4ehtB#njz7FPYx)nkBWI6eJWKsC21x z>;;-r#Ss1Whs7o##a4H)laXmJK-dH3HmIWH*2Kj|_y%Bslg0-wsoyetUon9&dPIDZ zdsa1}oedZ!~E`<@O0F{Cz z{ci~FbKnEFBr=8c#0eZ`>(GU{LlJ?}iWPDd_2hGiXLl`k70^)m9g)ILAdeIIH6rP` zZlR0xEc4jF`|Ktfv@|b#5=NP5J zJELx(F*$cQ6@c5syo={^Jc6cC5$%zQAO<|KYhU1Am6%C6TJUFDvuc&45=NbzKlf_a zxmM+7M(0-jm9v2ZEZT_&-|2+-1d6~E>PlRxRy4<6cLB(FT!HN{e)S`*PzT~%6%S<` zg%B^x^h_ApOh^i)u%cou^Lhul`8DulI3@qPl_IAzor`R0*zo;8T`uKNnXNK^CbP1xM^V>XDoh6Cr z-lIbN6NAjBos=l~`qeG(dMmqRL(Pt~YTlOR^krgv;QjU+(5|-Hp<|UbfaWLvumr#*vL=&a;(*yC z)13Izfh$@HB%zA5rzErdFvkpJ<44cv&}cenyL1iBa6fQs2Cy|nEg^0jT`&U(!a|`n zksBOD91=^*D5jhy!s1uxdq|PPXq=+q=sLEVpdTwnQY~C8Nd1*F&|+dfIqTl9YOP75 zp3H+rqXXdJV7Og|y57vTxeWI*uVb3(c2;%^0b}`r>S!{X-Ij1-dnY7Sz-8p|1w)8J zC6Uyl8XI|W`%cg{n-`f-=B|#RBHX7?Ak&)`pJ-)Yat^=l`{9gsF@DM-KN<^RHMhy~ zU!N?wCn(vdzH0_LR^zs4O$nNsT6p++{sSJmhc252zx?k9Uxg8qY+ue=EfW7v4k=Jm z+0a5}4ur2vn#bO(Q`3k#HTiK^97xb)xl>&#m%|l}_U^i#77@jhe$*1?`z~~3!C&<7 zS0?cB5%`XrS25WfhcICko7Kp4icv*ZpL2$tb#vv^W8Ed)VBn)Nf*>c)zAh0554%34 zY^N%6sZcA!FXU*~%Wr0JS%Hmt*VF>cYc?1j;tUyHc??Y?9S8>;A(k8OR^M1QOKM`b z12iaNvCQCmF&FX6Bcl;hsT7#rsGbm{@c0R^kYhk8Fd!gihH14C<|E8_>nH+N>XT!k zAIjpDhpKbc*3*|Y^moMe04Lml&6E-ePyO31;1V%?Y_X*!6u7H>`NfO~NKRnH#Vb0f z(baR<&By9Eu@i~6gl>e8Yw^c7{NR2$=0<1j*3+yF8B=n7M&ezC2MDF>Rjs&T!kSa$ zX0LDo-Mlfmfr`8Boy5S_5r}R<5VUSizqHFZfDRDPF+$6%$K9wp3q!uslmc)Zyc#|* zknb-brnU`m(@|eaq3jzCbilMpFf~I|+g8}@8Vge3eye0FKKB}yVWKn0?C9>@CN;Z@ z%2mPGgK$j`E;;FI#7^kng97)Cx!bh0R;_-eY4~#_7Twzt;Uq+(O((lsD}<-X%Sspc zIZ#-x1S+l|6!IP*_=yWp#6Q7TYE83(273`Bs#%den!k4(M1QR%a|1SUh7V>0MwGpf zSG@{|EaP~(y*%X@3LgcHG2`{5;!pAHekzAX-iT6Fye9fT)XY8 zBxPa%au;n7wkj_v`N$OqX4+&&v}a2sDGhm{%2@2e7w;T1vQNRpy3||b(@OLOZySKt z7vG7u@6B8ASVbTInGB`PpaMHgttL7D3F;H#Q&BLUYCNtwGbZ6QG%R+abH`nny7B7CPW44WC)N>nk;?8ljaLH_HaWF9p0=wy( zRP_5TsyZz97DTGc;XedTZu)oVcyoN|YO3MJPf)vJ;7!k*1FS_G;xf2m`uU8qC;uXgJaSmv99pqqw&*>xg+*qf2lTq)^!Zh5 z+yb3nf6Mh{c0-DV5TZ_f{do+5d51)JJQ=0CgW61~eWm`QGMc7idrn}leEn^|`KiIG zf^CGr?EcgkXQTvEXG)CWG=KyNtSXY)nIN0i32LJvBV(HvY)5s`E>s|Tj}<}|lN7S0 zRg0ArSURMwgxzO`kPEVa9kdV+)?+$=dlU|KUMg^jc=FjPEWt_k@vI(OMN{WP*-)9*ziovB!Z|@lL&#zL9E`aT2j^e~*?E?JjXrE?$96(5}Y2ZKN*kasWyEDCt<;v+*gqg=qk(8f>-TbT<_h?Y4G^E<{UZc0YY% z`+aK877)eTe8i{$kB;wiLf5UL!NyIp|0aM@03kXo{Lkr?H+tZrIMz7EeJ|&$3s1SM zH^U+Jtp;77N_@*OBsXC~kgiS_@+m5kk%|gbLU*n+$ytl19D!CtF21cU6mKF7SaLoo zaYBwquo#Ff;o#`VG#cRVb&8I=iboq2&=h4#&+`Lx2jl!JU}de8IU48!#U_#qLH^Ij zuAM;m+`_48RRJc!DZ&%Q*WBW%g03pP@s%B!Dn4!Y0skqO{lJeSH+gH+R83PFs`>Yg_IXQJ=Hn(N!kU`*`m%9&(7 z2}-A)b1GX})-x_NRl;zud79ARAeyptCZ;+Z2Ihoj@q)v02p5kX{>wbnq*095%DU~J zubl11p|+vp8II0TvL&1oEXI#PHkU8=o`hd%jvo1qWE5I6u5)O(*M@M|uNUEa8;A|8 z-ls}ICStE>OSGsr@g~+Rs5Ua!#PuTwSKI2x>Pfr!8M4=i${^iC7&|Z5zqUkThim|f zOwkG1(XleR_{v320=ql)c6%)Oal?f|(-0ok{0>=oe6EcQ z>9v!_Y~7609lXQ|T!sq*bgAr7P<@qPbkQ1q(CWeUNtePbZ z_d*0@k*35tA~!)x*HT`(rwn=ZrP%=Dj^$kX9VTmT^?PaWFyK$a?$E+8oLz+)F(t4u zXYy~c!yi-JX0dpcaAmAHqlobg=9z8X+Y6F=q}>=(-1Ax1hmwK|4PA!tOb8pk`=T_& z&%k=^ahie#DHb$t8oaH!GpModE!3g?pQfQl81xz>Efx}3__zfhU4|=SCVv6eo#WoRm9{^0Eti$@h#e^fTVAz!B_8%rXXKCM#K>N zg;Sc?q|FNXycFMEX70Cja zZ)R#9b1`909=62Eob>6VQvyP0U9!XMQ4yN^PA;7}28W&=*(6fuy2m?;0>}ZP4PTc7 zOrM2g+-uP)2MQqKE<(8qNGY49?MUO0|7d=%bKxbM-4Bol(7A)Is{~heSF(di*Wf`0 zaIk*Uca8X6lrBGi>rw&8!1pYx0(K~zVtoe+_?~CMP`|gmc0x@`{FMx~T_H*#1?2WR zQ3vv#;kGZq-k}h}rwGrL7#=&RfE;2TjoW9nk$d!1l4>0@NuwDgw$g+lQ}E)(d>B+x zePQ5BTh^^G4E!}L9|%GxSJS?X0nT5`qf0Dh4AG$M*0|;%!;_0;dp^e@AEaWxXcLud z)urc7YP+V9nrp#EPS2bztGu(zYi%QKBU5@5Nkfh&sl|A^GuuX2&?P3WPp?*>dznW& zL|3fBnRLZJz-?)1r{W$lkJr95QoCq9Bx%md_jhp%FbI1p+&ui_{amGci}{zD!8|oi zm`A{`B(luDF{1D44`=(wA~TJS7nJ4a)%Y*-W>H4HAgYVe>w@OthoJ?RAzDm$1s;y3 zJ6$FXvc?+j<#R&Gtw83M4*}DGr?t0;dko%4QOUc%MY}5~l*KRoQIu`rq$LAcyrkGW z=_G_Gaox0gFRe^Zu*}txl_9nYE;=<*gg)R9_9Z47<#t?HZ?Ty?Yts4vd zI84D98RCxa4)NwM={_F29MdgDH-_q8`~3u4Rc`SgSZi|ixX*q>#rvIGr+N5dpPNy$ zRnV)5kVjU_Cj;lsDr;Z%`jeugGqQg;ih?J&7WU71)ZZqxXFP(!iE>xY8FpR)vl)H^%Uxj`l@TR}R z$Owi`u9t+g$Vb_+VQ%9I>e@!c+uo>E6SE)~%Ap*KFsa}e7Q4g!_I^Zd818;i&u;X- zP@Ge+7P96psSA=)LA+QqHrjZ9-F3!*np3)&t(pr^66&m8IjAV>I6IJuKoW>xRwA7H zB|LWwo{_2u=g<%ls}$h4wldt{`Ym4Y=;JJR87~&Dz=^_|5Z(-<2reBSB~{MssPEtY z)FHxy1#P7W3?0XA`7l0zdCIDo)@n#waLqk!;V0}m1HtifbE35lxB$im*E3eh3mlWrVgQQ~vGhAc2& zI=1+FG1#})em$dyK~D%+yD{!R7U8EwqEtODi!MdeM?pwM2UC%QhzMkecesn7p%N(r zKPA-#;;ImS&nNwuC1~^Mo{EWN0i@D(uI{gO1Z?M5H$}Q@xY*C_Tol7X+o<%8!OBf4 zr_mzMlI=WyYT$N{G&dR~WLFM=LGZ52!uR8DeAL1fd)$`qUTntU6+y_dzk44XH4su{ zVp+L_K!nOnrR#^S2S;y_?bqX1=&BYtg;1c*#g(hzRt$1V*0H{)2rDR|)?YMOEA@rS zKE~ROSeunu?E6l9yxrL#4JpCl7Ik8ef)p6$YP{l%=`smX{km}XfL5Zn54dOC9uoG> zG`)akYqo#ZNV3AgbP%NF-{d!UDHfm|OsPJRbgE1-3h+*1b_Y8eTrkoWVxsyx*0mOW zSr9xAp2(Xenm{Bj&>5w&9%JE-sP2^m0LL%i6lYV8C%bFSxYUazB%SY%E7^kS4sgp8 zxGu=D*8~D8EH7@BRK*9c^Kx_i)Px|j;yUdhc`6NNfczdSk>y5-= z$n|O3r2031(HE5cGA!QfG`i_k5CY#$v#@ofP{`q@cRnD==*)F2HmbQnQGm#O9!)lV zCyYB$yJJ6U(wgF@#aqOeAY)i4bS9-CAl*h*944TF0AdSLO6!rfWQnS6a!I7Ze)*2LKNvw8VdvkwVI!(q-lJ1J=QL)6YYDcZ<)xe772!`87+ zR8{>hcUb0N-+wVbVby84h^Iy;Drn3}3pPHrVDjtex2>dkRwCk0~lq&OS?xz!#DC*AKokr1eup(l@+NNM#9wz7p7 z?b=I|(0#?TRBu~t5Uea@F{@$A6?mk_F&fL)PQIu3(0dq*fcqTQSBszm%AHIX8D4Ly z(z>`mUKZt$)c89#p^znk5eJRfrxfKVE1&>66qs!kcwz@ z_7jT%74-z17JQ*W{C_L@6|#pq$7!N>2XSd`C!%pd);v21*JnWrSOZ9+&v6R9ammU7 z5|=!9yOjNv(qaA-1K`1`9BemM(kg*=drO*~kEN9NJUuL6Z}Bg3fO$uNoqVWx8%@=P zyrwOK50wZUfiX#A1GoTlZI93IU&+_d_cUI1kiFX;958cbW#)Tnfp6HQank~j5uns# zx(UKz>RUSPO$O1;_h|kd4H}slKu7puOR`==p@Vf=2U@F>Re!Gp=k4JI415U+2#C4r zWVMB-&aM-=2FW#P<hA3u+d?m^QiJ9|A0Kgl&Y$(iMJ%=UU0bIi^arQnQ}m>*>vb# zt0+Vklw#beimq3F&M6oXCEWQM7ynTWF>oQQLUv5+|Ehv0D**p3NGT=@8u^H3180I` zoA%0z3)hyXd82bH_s7C9IE14(xa-qj4zoi6U{K;-1)1tTpWNb`xErOTGjJg|>72Bz&9f9>S~i<%l^I!0?(hcQulDSN2@C zNriE-S$HF1D-;IVe89etr1a#O`LPb|%IxjVc*#e?nUogoc{H$;Q1JMJ{$vtm zo&|J{pJ(ouK}6xged^dWw?(F}xSABAMtRhD3A&*D722^>cZ+BT9g49qM~*;%X}gvK4W1_<{TV zr8Pk3Gt%O{O}b`&=m1ssh0l6N>IywTqsUOhjUN4BHCp2p6dPnK*_GH_%(h@u*n4Uv zcH=#}Lv3>j@eA$-j(!!vxCNVYODKeOrEe7=tOVTKL=w^|_rnGCVbR zLpg=+#f+L8cW)BL(FQ3{8LyG&yNI)ax1A(L6(+@f4Th#VqpuUh=aGe3q>@uVqe zD}G!UM)THEd`_|7U1cLQ-4?XisjIGq&3fl_#zDbU$H5TMaX}f9Ew|YYFdQ{4jlFwIoF^lbMfko>AWLIn`w*)q2!)8`zN5`b`<`TR8^&=plIf4+^$=jG*;#gj zI!#%DgS5a41}A%x#=igVdwklyC|RmLmvi!#9(Cs}4m^#{1hU;h25n#763f)dYw(;) z+cxiwEmk&w&QRrx)}L_p)<{H%5~<#we0YEp8)C1~qfP_n@~z>b>cg$`IDj+ZYMGna z(3xBAQV%Ft8k3@n}y- zt>PtuNu1R3B)EkN^im5}VXAD+?7Z|^E(gaCmP!rhHYVJCxfmE<^g}f6w=x_<;R~%WDKW;AioI)u4)Y;A z2Mou+VVoO`v(mS?>USFRJlZSGz`;BK9n3BU)Y>Q%){}D&_B+0Mdf)<}Q~erZzb8!} z1r>MV)PLeThcjYvu?rQzMW$E`%kT!45sP5r2G=ti#(2EE$Axaw**u@t+`HhQUv>&i z6ZdHXl(2i;0S|VfFe&yYZ2np;%%!ETyW2r7#2#c~!sn!aX)ly!v${F~Lf+%7#8Ce! z8fC?2TpL(FuN3*BqPooNb)N?MCCjCWi8swC6(q^kZ6fCdELs#&>7ONdtEy!A<3yoL zL||$Sen-fS=B9RB{K#B&XTra49dHE8Ggb(ut7FzbFS1Xs=2t@*#Bn8*Ynf@&&8zzX z7P_yRumO`eo`Z=2+vw57D+jN#ARu~9~@*U95iuiQ9YGI|g1)-D$HT*86nAd88k;zpu?T zB6ZV54JbtDWx!RC0+3m$cPi0OC<7$%#$n=m<&jov|E|B!y(j$>V?o!bTvVH(0h43| z7u!Ta;zOK#NCSXo$~h^FcGfs$%uM2qXB56RSmI)`U2gJX%ePSmWZkuu|HGN|_iY0y z5R{}K)S-)42C-^zT_1Getwoj%-L+sT=GZ+A(k+y*DGwK1J~>xGM1y%s&pXzQ@Oq&m zHzH06wC2->M>}?Nfu=LQz_qnCYiB9NLb${Q=68#zlwf$F4CQd0fCsr}$Q=|6ntR3g zTG`aI#_(J(v=B|&U6MB+uMkAz)!8Rkjvc`6X(##w?8`y1}9v+C}P`+VwGszI#JV zSy&dmX#50Jyvn&$#lx2w+-orQ!K#^Q1<)8q9Vy9eD2S`jrV#lAMUb02sKQRJE9+y$ zzJ7dlD!KH$^`}U$r4+$#j$=GPn|EH%dM<;idoruaj;38UFM4#X+)EwR;> zTBf!G3keE3F5#Cd1n*nZb7!t!7ET(t|4QRG z-^p7<2!M|80O$+@k{!+?Az`rp%%H(6QVTjmx!{?FrGn`q;c9nx&(YveI;JNUN4yP( z`wHzf9`P+YcMJ9WCWAko+Ao&wSPw$Korfx9IRcUFS{Sg03{^5&gz0;Q20=+E0sp*< z3kji0OAQ0Ai*0`f`VQ)AL7L|G-hwC=Co+gW+M>X(hTy*}V8zL?R|FWz?eETIn*kk+ zokUBHH<_F3Z&=9Q^m6sGlB|?kvpjl=_-m{5(csbBl4~OzRQ_GJLJxAx3N)&3QMd@b zO~&<@U`DE|gQGsZ(o4XTFo!%$z`?pT2tFeO4{X>iW#{2}15XwQV;QU&ml*|yth{s! z6UO$PTTDAu+{E2gvGZl)xV$bN!8m zlR-HRdfkcsCTe(Hdk*2Y9*}FW(7SYtf7LvxNu>=e)vjwteAmXE-5kOc^m~7}Sq|@( zHEbqC%4$3jw_&+OLISw3p9R)z2}<+CvQ4DVlf(@RF-baDRHEZwKFoPK2TSn77u>4P zKt!Bn+&^nrs>{*nl7mhb-j;+|?9*lQfD^%U!6i11lP7=}tzGykP#~DvZLL_X9_p%y z8U}94w=lrfSphb&Wm8coAPDLMv6 zOj@gWF`22}xOH$b4n3ak&7{lwMvKIRbIgIjxWkd{=cQ>&V~)7KjJHuf$dDOrZCEJi zf2N~3J0i1u*>oGf5XgOpc`k=iP(_d2&y3wMZLqi>K3PvtU3fjE^(bLD8l`P9OMCrv z%u8yTG!!eP#MN*~wJpMcBzB?C%-($wpd=U2zKQuIFq1|li?w)Wp@qc;YQbYeI!|CL zu4DMzG=4Kpt46nA&?q?5itANAUV*u|He}ti@p5*(g^ZCe>NAgR86&Q&1;jQP!*@Z3 zQAb_RVPkH>*B8ab0=|PE2!|O~&@KZN)Ce@X9n8gT5$bhUf33UcnF*-FSeIaonC;!B z)|*Fj0JAm&vMIDkztnzyFjtFF1H=YVtV*KBIYStn|CYjlYD*O^OO3q%LHgr-skfH; z!Ot4NQ$i4s733l!r2y?fui`x&fiQ!|Rw@n5N_o*8-@QbA26ErZg=-{TR%ik|q*GKn zMMEhIXaGp^3DDQ)Empl2$a^Oq3^Ad zD%d&cLZ?vwNT5<#1u6Dy1dmZ%Lss^(-T237xkySG=YXgCL+H0Ng+i<$nJK}AxO#}!L*Ckg9YV!eH&;0(eGLmt zvFO>(#_kBuI8U7`d`6GC63n7yWD>vwO7C|qtLB*e_1$JiY&T!2djqNtb8)OW`DXEV_OGaL zhyTo|Qv1C!!>Rz1S zZ$if*8aFvJ*9UuIDJc(92$SAA1bU|C5t5f`&F?a!PQm~tdz8=E^L2HeLJx60Uth^N zt|&-Vo_+$02P2h>&dwBlrM*WGl^>iddu%DU${NIEK?y|93K9)u%qCJ?Ie-JjzS}@=m!rb&Kd?do`8Gpry7o9NW)Zq7sPbQ|JLzBB${q%rXgDTd8 zkgy4lRlWsKD5sqR#Cu;|;;^j@lLIAMo%b!YfKadOzNA#8PgegBp{Jr`AY2Oeb*Fl}C`=z0KP9~%(d_+qVtb>*fut*2OSjds#Bi}M7HHPKUl?0D8=8}oU`iR~G zfBx&mP`s;8Eo3OwDU--PCP7I%zsBr-5bLhP(Zh+aZ4Vz=pIOi!JBvyPHnSW*)M1i~ zVpoh#(fyMq_T|HL>S;+EZVy9S5eqZ>GNSUHpU&*@m&>*lESrOPs|(Tx8WANz$jXcI zU{OyNtsz=|VR|VwPzG(g^5uPP?Jo<6v#7m7{MeJeomi-+d<`KuJCcb09_g;RJSaPH zgY?|B=TJv5%W92Ay=))L9NjJ7nkpgVphnXSsTINiN-q5!Jm}jIR)-sf?;vs+b zpb)JiRUP-~Wx$kV-H2xtk*!EWxL!0Pb{p}Gh#r6net&35{V~Tmy0iX8LpnjDGeX0Xpqp^^6esZ1h* zCyXdv?+-fB@?|YC@B)mTBpX;y9qZlaXWN6lWU00+jTOtkmo#bp z;wx}UKZdFq4}g~-?zbc>Zj7#*-S?G6K_TJqQ7unh)wq}T3`GgjV8$5V+nVq-Sp)25 zeJ)V`<>@w+*}Hq*-Hva(Ck?}lDhlSuCSup{k|miJ0iTkBMAv3y9OC_F{fV$&OKWOm zM5kRcm9ol08{bwZCQ@b64ABOp!mnCr*sr#dFeU5lnb?wOcPW4PEk3 zo!*!&oYxE@{O%-M*cy{IJs)-ekuW+rnfWSwR?QMU-s}&kcnleP!i}KnqYR(mkJbpmgvp(D1kS7&e@^{4pXFkIldTzvLy3U^Yiz zKfq^9brLYsgR?Ahk*@hB+Od%B5|n{~2NxIN#9qJ!(SyY+`ukbsFjO6>{WisE^z$l_ zp_PSnS=bfO4hxtb5ia@-Q}5)iXd`ghSU@SGgI0m?^S8#snL-*n*csX;saafUnzMvU zlNx4p%^*3>RJPZEzXusTWYyaY7Y6qS-~x=BFTW;4l>NP^Qm6rMWOGpKzl<Z!=ks|;vG zj}LlR<` zD7g(Gl<`#Ii~$_MQyP;0&W$Q@=LOKWuK(NMbf*aO4|k>9;ll1MpKPNnj!oB{zR zp0(JCAo1@9-6<>=WMadKv>a&$es8-n{r{jcg@9s-w|a9?AA2+pZddWnnq0PLMmmM@l zC6EUahgqDU*aU1^r+rx3FE@|LkvY&jPYLi=hUz$Ql0!SZpj({u(AJRYv zR+j2?T8t|fG{V@P)vLvG$FwDVW4j(Cg*r)rsLJP`(}>$+R4kL`I)QkYcUlBS4v5aM zyC&C|h&%*HTdLG@@Uu8|Xv3FC%6r-|UxunWaqK8onUg(C*tI7Hcg&*F<8=iYvz)^d zj4G`KXtmZ$`dOMZgkdJ!a*1;JCgtxh&XqlFiKP90Gtyk_zzFvgE2Fey0WqP1K&4uqz4K?PFAIdE9>!)CLyY-AU^TEeq? z@5+EuP`FMzK8*-0=zx5A1a*g$q#oh_vdk6T!0~7=WDv4u0L^CV-Q8rm-pxLJx;!n< zYA+|cna;z&!Tdd-Ci&x#f zc&a*2NDXEdYty3;g$bLKWtLWq(L@bR;i2qczRt-^{-7pFBRQlTic|xn(#b^u^vo_? zIhz&2tA&xS)$0T6>Z<33K74ip7gB50QwE73XRx8S=DVG?uxCUysfK`4l6L5|y-LfHk50}RI zHL{ZK(PQ!i`PTjlo>Z&o)#BRt5?T)-9(OhylN^QMD22I~G(8RPmsy>X(x;5j@aaGv z8xBq?cW~GUkR}f=+A3`%2tu7MeQJjPIo|<|3x@B+Lc-VFey>Uh$^xa zduSq)dlr`OJ7Pdb;v@jxcVLUqFZf+87$H`4%LJxcDpGN+Jv18Y_6T$1s~}LMQrXqp z@&0D7AAQk@u#beQKO-<@vev=;vEV`HkpCa;skTZBOiHj$^jn#wL(46;wFzU~g|=mrBt^ z{k#!nV3spTB-e>ez=Krk8wZl>oOAj{{SgK*1w6Wl%jB}MV%Zhh@4!o9RJ(K(nW!rf>bLcFu}g4 zql?_T5ZD>BmXl{7AW^qCd>IPNOntFS-%|PO$)*D?KFirQ?0@OW0rRz}Nzicqi?(;O zAXFzO({n^U*z&Dsa#eV6u{IG9^ZL@8K+9Pxr=)bB zLA-kMkRfj{C!EJ$6|exrdMm%(9H?GC6nqeshQ!`o6md_3{5j=_@jmE2i#dDGa3F^f zplBvS?=FYjzP;OLQ85 z`T+=B_02=D23W90Pe)~OfzR*?HjiML!4WC|m%Fl^rD4jd`2TO}~fP4-xe3ElF%}O$D8Z z#C70F&p{(xmCe-&Q`o=5a*u_(!;KvbajP;ZvIdcD-Ge^H(6X+Sg~Pw365*KVzgSip zT@$z$&O(crU1nhI^f#dRcpX2qYPXMOYJ+v;>@s&;u3@}c*$?h$CD1jQg-Fy0LD~S| zB&LQ*7Yy^yBJSt)ZZ8+$X;;tPT@#By9mYtmENaY;Lop87iOl%BUX%@cVQ!Rm&lSua z_X`!Q?N_{7QK64ZNIn-VGz}{rnWy;c$5fqJ4m}SMin}GI3gB1JQTgy%jPz1UA}-mg^jPOi&xi+%O{w zEt1sCYEcWzYk(-%N45}kyb-Vx0?kk5{95Bzk^)xCp+h^C;Fp!V98F-R5hkhR=eNHk zct7%J*iQsNkpm_a$|2YC#EAQ7BYrBA#stym1G4Y#td0G`TBlPe1hI3okC*9&=sqZV zmTWYWfXE0aQ0IUTDW(!dj%kDT@O{ky@)SO`qJ!e=bc`w8sgZ zp5B5nqqkApq>^0%6e>M-Pfi78pIZ#oYB>R&Z_;j-|GU*ag`?5p4#?r2-{OdJUWY_w z{#^E|?S+ZnRJ~^wqd=b-dk98gA_m|rxX#K_Gw2{msrmOeAK)ura?r$hIfha9r~d6B zpBy{c{(>lfHx^OwGl}hC+^2sQ2PgL|yTe~$stoNmMTy2wsNb#_3Vv?vyTYNllmd1q zif17e=RN^!8l@6*E0}5dN%Ze8wfsc83=%*7CQj!mpqlJYY{P&HX&A)= zB0XqQ`W>jSOmY2>rNWd<)LGs)gwsZk(?p?r8>#4!B`z{j4iSN&|B*=ZA-n0LGwOH(RJQP)Lu z>~!hq@E^Wfd z8S1G;_s`@u^UJr(q#2k3u~~E^-XGtje!S6bzOGhJ5OjFsD!hQyWFEW-`uLc*tch=P zV<_lU1Q&ufW}os@d)@xX{i{IF(c#eLR6X+>yEp@*s{IdIII*%vvn_rZYq~3`nyu`-NfLdWU)6pS2Km(XfE5$6- zgT3QE~hiJhPl-FBBoCW*SE^`9vY6-==~|6@}){rS<8 z-@3H;Uk-j`?=7A)0~Xg8DIz3eo@V&WLzm8Os!;hz_R}R~4M{HQ#*ppI+%9C3DqTyD z6F+RVD-eFGgl6WLG*Pv-DK>jxPPt)6Q5tt4^6P>+{XQ3J<5;Yu8q(PgyeqVaL~ht> zBfd&n+;?mYclu()w4}Q0vY%+_eBf9Q0}C@<_2*p`k-^|Yh(}PKe9O0Ab}vlDi-#mW zY1Z49&=FP+*JY}K5-KsDEwfOMbc!93`WnpN240HQeVpQHK4zV0H%gtcS2<_{Tu-1X zb~?XG;_mY2NGj`8!&mssU;E2(HqHQ23-BWbXB^Vc;`?x4iWumxgjov9j(V9{z- znY#a(iiWYS`|o@x;8ei{7ya-j>e%wN5}}3#*-hZbiJvEZIbPI2wsUa66&npr@Z{2 zvxbLpi+1sgklQJN%Rzr$sFRlB?%aXQyAY(C|a;{m|NVzyfbqCoZC|Hpc>Pq06i435@y+qc!bOW1I_F2k@Yu=q4 z(P`^nfetcj`@&>GxsH%s)P5&D(mTsg{S|f0rtUslb@COuj|8vPC|%PMO`v4Gqs0V# zsAWA;!Ym^b^&?7HzzE?bBnxwA$K?cABvxB=PEzZx>9{o=4n><*Va-1CA0bUhC`IqX zXZ=NbmC6psly#w(gNp-7v41}|yoJ#b0VTK$#L)Xx!c>t)JPkbm5~m~IT=wx8Amh%# z_@GO^nxpYx;W#Wnmkb+BJbCUnryx2sb1W??{T%a^j23)k%p+c}(4U7G-E7Ra=-AZ0 zUTw7hMni6Ah=F;6Z|}^2t_rg>#?IobM2Y&NT$(YL72k(U2}If%Pg7w-$gg1#Lxy0m*H zoCX(0G&%UH@Xve&XX5NvFXis^eKCymfdH+6?u5C_UKS1G@bhiWU{YN`v!yteI5N@* z8nw`9t%%D8O*~9Hf+T*H36;!h7fC7`9J%F(T`8w~W=De*4GlsZbup1iCa_zzssR_> zuHPXHmQjC}CoHsQ;5$mMS$YU`2PU#5)SlrkP0&w$kl@*A#Yc;4*LwUuhhQYfV4jiG zLmGkxJ-v?$C)y%D+vY&8wn)dQeP&sf6;7q$_-1p_$xCfsu;g`gQaFVlx;VphtB;;{ zQ^`rdAB?XZmAmg>(mKr7cbL4tWLk6k`z-2oVL7Q!jaLK?UbBHIzFW@mOq9nOD=2JU zLzW*3*=i$pjz%KACzki+_c;V4{)H?N_DmD>@Qs{i?C zZzM;!d9RtL(Tm(vR$;oKmL4S7Z21BN3-DAxNpXsHYx+bl(yXbNVe7JeFESel)^eev zzcemQ9qy$RFqT`Xl(!9Ol33+SHDWEJ8~8nA7whR$9#2tZdS8;8b5N*@6Zgjdh@x$g z%e7s@5K152)hMR@B*|_Hh6sqkDL;x0_Lg+&K@a&*O+>ip?@FHilR-vTZqCN;TglG> z^9Ls|vOrt}<9AjiW1aoZJ+4nl*|kp)*ntgtYU@wgf~-D$21jZo!Fhscad8uEo> zc__WfP+Hwi^K5l&uMwC6fbblMl#2UGj1K7Bs^)IKfq&Y5b_M~#LfnW&+gWcS zV%|6Sfh4W@Hs*Mx8r&hqK;f2{t_OCzu0=*mOpJ= z-|G@>$pd$^7PI^`*5OgDCcEQeoOk+y5QOypC@>CH|LK%gT;QcqOu;vPG2g9LL$|55 zgguT(|CKEMNUVT8CmuPGc@ZnHF(Z8j&-?7Bn6&aM1Avm#NjaD2`5cMxbN zycQPr| z%xI*J>Mj@hu=vIlGXblubB|ISH@Zy-JZ(Q)weTvZ#`4~@wa(>0A(n+p?Nu@KEaU9L z2eo?yR6Ch+&{>C9Aa_RI?$$Fo&6=w5ZhFAvQA@hT?o!}P|EtZU-BQfmGiUi;6Hmkh zFBt-D5++*L(Ys0*-RDaLfE(HJio{V4d4g8?bh|xlLn^8QkdaBa!oi@_#BV3Uw;9Ii zifGC0SG%q{P34y2)p3boeGt;JMlL#NS6q``+mezEoxSr7D1J@KdM0>bf2^M&S8vHM zFfzGTkxFa1ODriQCij3PsRV$Ynxu=1V{^PVcUtY#6noF!F62Dd{&b`d@TmM(u#!%X z=U4l~sXrIpxZ92&*NCODE0wg*4b1r@Z|4I**2VK)iT#iw8acPS(uxpd5SI((qCw?! z8EYb35)^xd36sxn2U`m(*pfqHLUA? zhz%nq%6(>-Oe}fvQc>P_3sqd`-GKf94*R?SR0t{?tstpp^szoOu*eHT*H%y)#lAvB z26AuZvVW?1rU9-AQ@QF=(XKQMN~^7k}cqPfb&9SevCLNd@~PY~2d10B8domI^%^$s&B! zr?5d$ULGBeImIzSU-mf>yiTRbJ?fe$O;8y%GYvEf3zy#^$>`I*dppI0 zWoeHnK|4h|dt5^lE2Tof*LcgIU{QjK{+~-Q-+-;>pdv&PH7@v>50(FfqbhNw$To*+52sj)K57t3*{r!9tJh~ zjD&NB2E23`yFDjS<~1JVGZYv=+AWf)$Hpzy3U&9{hHj%a3zgc>n$RPb9q03ua0HHRz$jEWy$jZgy9h78q_UuZxjfO7 zGLIdzQqBXKgG(KZ%?c@PX_CK95bBY(XG1hn9p< zbfLF}yJE|di}cxwe-$th6-ypns1J67ax}iY8~4cHpFNEw7QLTHK_a{yIJAFXnWvBRASAHa@0wjdJMD_9%uXWd|j}fv9mmRDyw${$KlS z1td=<|4jY#LF%^~Y3Ej8AXO~ES+4sQiLJRhrVyq6{G&`0e%pI7taWSnR+!nmU*o4E z!Uxym75ajM$WQqGu>>>;)zRWOAn)FlkD8`B&If`e1l62C^h_Xz5+i074t{vR>_Fo| zB>;!?=PsvJ+4HSd!vlu}ioAB7jd24}avmzcEBxdvUg`f17eQ>X=D;#>^t8989^a?L zm=D(utbPWm^vsGl?~2m9Q`0kP(p0|sa1?@w8{d^dKftCJ1Y3bO?Ugw6wU&Si`$XL= zTKq^d+*bLMo4D!3y+kV89L#M2A!eJPpZ+1@4(vWi`8*} z&m7!*JWOujOQACPK=)LuW~}P4T?LMcz4x*^3e)LhDTTIz z)+LE{$tK~JRU>@ge7SvW_ddx{h}-(uvzkA`TaHYmyjzsYDUNm^r|jB!A1`iul#M(Bc%&)DJQuE?eG!D7;lBYgSJs-ApT(2#}xVtpF>G+C^`ppzm=)M+b{v`s&T4{id6akMzD46p)?0e*T2 zhj#Jg?2q~zsiu7pswzLfI7tUbct{c4O0 zTGhcFon#adB>@|NMjLK$Jw?^)1|7Q4z+N&qtU4FdI!T7BrCx6-|A&7lpLF0`cFte0 zU{TZ260DK*N8`Bilbw%&jT8h?Zg)piazP2*euI^92!s zMuJzvH|9m6iFfnd0hY@acNakQdhdHT&!T$OY9JU&k$2>ce?pmaWw@4~Jq zV{yU|UH_@74l`5~kTRjV-Ulf6JBhPBxsCK$t=nCU>-N;5xs5}DnomA234&Wlq49vG zlfh%SKJhp8!bR6wH25J-wByU>n_THTpA4gCmhLA-1tKM{k02u5e=+XrN8k4_qlBa* zHCBtJHlFEd+M=YvJQknrkVIgjH6W20-4Jp-QrN5XJ0j2vNE)EV+bbI;O4wwtoSy+& zXI&!K-ax8mP{l#C2nE1e_c7Ln)Z<>K$~j|Xz8IfeW?wRaWoZE_7rGPRAbkfG&EptJ>;r-wWX0gaip1Z%91)KRI^*Z^llDbW5ceOoRs z(h*E-GC{=aYG(~ji3G+DytiUjC|yegT{(oC$MvC?bIXZe`}Hp#t@co(x0vHY#BwU~ zxQeW~?p>3&cfgf;Ka!@%R^qv1FxA=et*@DqilXET|0+x~I0(}R%{@Sv%%;rv=gKx! zU#l!YKj*AXlxeFWY47($RGNT&&Vd2+fCQzjltZLrgzo}C6AzR_A=EgO6spnEf#kVB zD%5M>^C|j&=gVvblAcMKVzN}l?JAYuL;h|_m+8#p8j^S@elH9K5J&eHZRsdBHuBX4 za4pzhZ;g?5u_7S{0IULuJ;AP6Hv$vCc6iCLi)8EvMZC^2X9;LR=-R4b1>C;6UV)N$*71}u6@ zgkvUu%$vzCIe>xiS|lj;S7W|dIJO59cZ%{*KUn62=U@r_Fe0InqI7P*DC!$r*G-0( zp>~P1Jix-u5+g>5B(BewjLQsk2}a(GTZSFU^@wT{Qx@=KEi7wJUJNYZ@2o>g~Es>AUJp*ETN29Sg;WQ={i z3gq90$GU$7Zu9NImIVS2tsaTrY-s(2W4&am#b`^L^n{=p^jGd)3b?V2!Bhu+Gbx-{ z7dYN>4K4Ou^8MBI(>FZvOT%}vtC%l<}VX=ab!4 z<1yi-vD`-7Y4kv&Kj=vq9f~T4@?LopuufC`9yerFXMVf2*^x>f7_|dum8K#;#RVeA zq*mH&>cG;mLgXOkImlW#cK?Hp@z9>7$9VL*$^~*yqBW~z*5}~*9ylWfy-%Zzsu!jY z=F+xt2S0yqkN2s@2dCH&_yFP4$8%z@gl!yew&2+QU*x-o<$r6WB01phi{j|Tfd&NW z=%(zpwj9}io^M*x$dEhkIw1;jvx$jablNlaTLtBGujvy5GKlbI&bLi*yG$Y6h|yhO zsW|EV!wLMuV~>@H>+cLw(s|Enocp~u8wS&qS`)~Fr5FKYpN$DX@?~eT&ExNbY>?STJrDV+Cs4AB07-!HDE=C)lk6hj0M zK)z%%#k_fXX<4C3L+%pS63fAho;5EesM)Gs(WWY{WgCH^cTqr7NOBvLEnn3j7DAi* zKc-`)KH%4x!Y!V37$YQ^vnDymU1(u^7ZBVL)Rqt|2DSOVmbOieT(J?xr^DnE#o?4H z4vdX*4K8$0qPZW$4q+1>cB?30dI}-Ty!@r^Za{p+eAeK|&jtww>7ihF!YPAwqS6TT za#}0iemf$cO>Y%5mm*yrO}|7Oz1jp+C-Gl#x@{M|N{%-oVzbsbnDTfykDOYcq+&bw zx<^-4Ibj@WXcc#p%@amoe5{%~CWo}6#p?Ypshz=1w&GqhC37kMm3ArCn!@PF+2%nV z*$Pu(3pOlB67RZ?A9HraPx28LX3ACAr12Jy&(td1XaXpcj8J}>JSVQxH(PHjVP^hzYyEPFV=%|;sWW0%8u zmPnzP1=$P9k}Xg;eSr;Yj;GNR@5g$zO5cBXm%vDJNwHo*PblKVm{`^gih(neufcU3 z$29C0PVon-8Fi*@mS_8C#x)r7gN@qE+j~uUw61E33h4BmxzdGV6zp4i2$BBiKehzH z3stL1lrzk}70AKvFio-3hnKbHxl|luJ4PRme+=rO(+z2=lf&1MoV$i11F+BNQ-iN} zXPh$zvgR+Uf-9mQCAAt6ME8j|0&DK{0OG{{&v=;Vo=nhyxC{T=mhsv_5%Qy-=Y9lE zc?th>(w0|kvC9=PW0X&4U%4?4#VWeC|EslsODs@UOU;o$R#?Q949=q3Y1TV+8f)l7 zgn!nHKF0U6Ne1B7S|Hqblh;=!1&Q%PQekjGi7@H!{quQnj`;^z$w%~%)M0rp`%e%+x>`3|r_E%u|t&+bK?@U$(HH_v&V0T~gHA`7{ z3mAJDRNo$1)uAx<{PZtIIqv!^YY=~yI&r(}7M-FAlaKANPSdm!HM45+u70wztW)5v zLVa9+ry0ROPYCrczDt7l78`^5Ed`3IvMRF6?y_oPg^}*dfpY&fY0wP;Sc)_4)Idgs z64#vu&a6!Uwen=&LMmV69lwNRmSj*$|BjRi!OqQBhyDWz9y1n=T+for8V$B%fs|;K z0jdu^Z6#zUXT{0ek>H(?M#!3&#eHspqwOI!MH%y^oWpaH2TKHTpmY~kw-LPa-Bk4>w!O!5insM~=2WfbZIu0G z39(BOyw<>;6kvL`SAXxqN^&T{Zd7KsR4Z@g{d@!pVCdps_Nb&Yq5&5e2Qsstsd;Ep z@$fAvu3UfS*Ru7Bg@fHka$0tqJ?755#)oL68Y>9I7efJO;78fsTn;F^M1%vq?ANmw zQia7%J-C)L9$HEzT|OO&Tj{HI=t~Dh_#8K~VK)J{hE9V>)^HAxvG`lN!W#){+6(6w z2mnZQlFwW@OuhvuG%vcN|6Y-9e+6Co&=}xwV_%8H<dMe z@998>IGpH@v5$3_@EY>Ff>4kzJlZQ}eFI&-laFH_ZL+BW4T&wgaue(_{?Nd#4mzZ< z6x0W82a-^X719fHxBIgJmlY!^3e!qC$~c*B^FKpL*s)ohyh>GwWPcmxJkAD6SmgVp zP8OH^5-uh~D4Od^LmNQD{V(ZluZ2Rs(S#6yumh~V?KlQEiTbbm4R!t2KEQ(D;m_@U z&R#{$?Dr?+gM}yNM78cEJiTU%O@s4WLoch$KPvpTTUtP37A{#8p6vYD(eT48VojAN z{Y&f)^|KC-AJnv9xbAo~`b!Q5mXTqk=gcae#dcrky0iIQzuTyZ5=4Y0^8e%BW_b?v zyxQ3m5udIT&3CLgvzc;4#{iA~aAr(i}h zZx8+~{J-+)P0>|@W0{`?Nx(oTwjLZ#I?j6Hk`wt`B|YU)c_<{qtYAV+0;QxL9s@kr zDYXF|2)J2;p81$awDwx4l37P9`f;=RTHhQH#L z4k;dHL+HdA)FcI1Oe*0JW?9e2=vdO|-TGW-`aiXbtf_d}_qKHm!$|-n=9V7v6e?Q) z;Q;n4T{CAs+2|1ZBons!vztb!a5`o>ShPA?=*5Qi2KXO=_q+>uas-Q%MZoz-I zmGeRKn5M)>TXO#xq|uri2Q$W*0#a)Hfw)9YKK^2e2KYK{*<^n}AIs76_>(BS3~tVH zHQ$WE)C@+)6%hEPHQY92iwJ|(mTNSlSO*qr{G2e~VY3jG=!@{H#Uvk=u#^bRq_01bpApI*iVi7U6j6!Ju zZHa_9iQdCV$IvIwnI(n9q4+&~$=Q5+!V0Q5j+NH&1=I%)zv#Vo z-#kaXB&zy%W$XbAXeXT4bV6g#GpYxPE5Jo&Aowoh7Hh#p0X2)&J@HFFELot!P<#X4 zkMhjWsm(>Ftz#HF7Fs-uhroG15uN!1N$TLSCIF9uW5tf;zh%-F-JQ?#2ZlQe-_xxC zgmj|xglpEKXEOu-BI^!p13uokwR9_+a*xN3LKo)=Y&0{wGSer@$7bL?T)RLH1Ccyh zpD{~KKTl;6wJz91hJlxYx_TAWb)bOz9`VmLhfX)ffp8}%D9KUR))CpnbZZ|8q~Fi6 zCc#^~buZ)bk3Ljb$>n?64F|9&LqNl01ndW3$eXV|Qz9sW5li3vx86%0&g_~~oM*Eo zYkB%81t;5<6Rl&{t!CvS(B{dCvEzcWtnQ`w4FXYYXJ<{)bC-PGF;VUEBF6ETmOcI4 zY3`Nj{Xo6x$d}P|s=FQNQfBD@Mj_-{ANz1|N&?beAFI`z_AUs;?g};24^XOzvjtS~ zxl3M_VAs|+QZlO9rHBrBv3}&qkXDDu{Nk%P8dTrr8oD$HPyQZ!f>L{T9S~K1AU7K} zhTqOH8g&ai#D^6wBE1g-dm3(JM9NZF`gy(@0{FLLUtN9w(ZsGlf0eMzcoI*ldvaIK z;#LG&ZW55?V*vaV$Vid)^jr+6g62(^5#fX?1F^K20#Pe?wRHF0nS&3J29_~Oo+Dl3 zbqUcflTl-F^8akdI|~I)awk1prHrZ<{Pb0dF)2!T#1~BU+#(`o2i*2O}YFe8v)ubH|lXLILG3WiOYEJR71AXI`WJxHX6-Ij6bmtS@Zo#pFOB_4jZzbdj zD)2rWY>mtE7AKud8<_XbhF@QL3=pzR!Ci>0477_*B9-A@r_PDG>Wah+quQm_k{on!z&#%<7EGkiqpV*{xR2hWWCW-#;0ZqSDcsvM(UdvgDX_JzL~->I>K=&a6)9?O6&itrSn@`zNptldo`L>g zgU1Jo!#Y>5K0U^vU^`JTM73X^I{c6izG(1G-F@_SOqVnMUKGbb=%}O{gW-Z)D**`DEq4`lF_1QpQ4*-%WxYI#?VgVY_x-nKJdvCXu1% zn&ygiX8N~`!U#!a6i*X(+B>KZ!X9?BVI|3sOnYq(KJ~Ek=9_ zae;2A!dBRBAp@90A6xm$OcinO|76R~*U8dXIhEd8dmar{FIfwPNK)e1hm1jj>W_a# z^TD1{6p|C35)dkE{FJck{mihtYuB4XAHqpil&+-_AcAFPuT>|gp>=EDODhi1Ly9~t z$BlW;4vP{eU<_B=SCh<_z^ICYhP6v4v?vli-LhbhZ9*Bo`eyi-bH|4x*KS!GuJ!Ooe-{%qn=Rj;KlBGQla^7;LQi{b>FB+!IJ$H1fvRg|(mCUE@zXRWU3LR~ zUND*ZVOoV~{__kVnq7{a4U1=MT$MX`4sFl&E(b3LC1KCRVXi4ykBgP<9X%aW7px7!|kh3mwe?Qq~>6wr%kzZ zPk*=hF(m<^h{wHB-?!Pmq8029C(*|Y=yicH8M>4-Xcb4whE49(2rK1I$qSx{P$(JV zg$qy{npfP@bEDlZ-_DLrSPe}9{2P#?czVp>b6O+NEbz_Y`_?Lk0kXO8isVa1R^h$3 zNrP91)QOblfA_i&I6_5kX$0^P=HW?(GwY9cF{`31r z`VAjpvDZ0C767*-FCkJO=EVS9pSWUi$bO35+_hau2B}rQP1bC7|pP1o6phu~qL_d(&Bnznd*P>GG1Z6<6 zBFK>^^9@Kg>)=IY%D-Tu2S3IVSN0CMU+e^oT+%>s)}rr)KiNVV3K+tbfP29^MAiPO z&!PqR4)#oPuD}4`&ME5d|1IGOaT4Z_q)l0KY%|gMS$+#ynyLTwhZ3|#+#xMGP|;9Q zaK00jdURu>2r|<04fWz^4|4dj>C#8xBOF$Bw2l8zGwi};f9GC`kU8w!-)QSQq!863 zHSN^?;qE{wMTIk^t!`>CxMvo({szQq#C&jh(HzpeQ`;*K#dFhd7^D$?pw|z~ci!KW z(Z!Tfd6&rIbiW>cN0yzEDedKJYSlE;+$`AA|Z?kf@M_txH! z!!S~*EjEh8BbqI8p&bfC9{)Dm)-gpXEO}*D#(db-*ikfvD~3*6jJ8uXs)<)I#Z@}3 zA+Nz;euy(MDDnw~I#4qy5Q?Y$N+m`CfMM;SLPIw(!wjnOxSlAn%=mb=7krAlJ#;2a z!o7we6?Y)YiiAu7T`$qd#PZ2f0?|FmHm;QjCdaK zA+&(?$2$Me9V|ld!;oH{{&|v^M%MgQ3}5+Q?2iz?XK98pE1DHGydAnBKFY^XOs?}s z&qS1ASB+&8sml<65rF4Pea73 za=hmIBk=EJZ=Eni*Z78!%xcWg=QHCjufIE*m_i% zz5!;x-(S$nVSa--M8-qA#<`BuJXRxg_eZFf8fhK)rQWJP>iv^bp2Xp=YGc| z^CXCB#P?z7-MEY1ATj-Yi09yO79)Jz?uGd6p1M52@Zo7yTotdqx$wSF3+n<$cnP@- za5zTEKJKB9vS3nlarnPWJtMSMsN2aU z_J&q?s|$iN8;YzC?OFbC$r^kB9VH595RZc8KiOQ)ec| zb}$?FW^CKGF~K|%wbcA_VzIvP{*jw2=Pv7X->&C`kQneWWE|J~|I_8N72QwI_e)n5 zkXhd{5ViKp0C<$524=6Q0$$f*ZYK9J58ZZN>L>3sIcGw3%#_gG`uE8G8)t_%{sMzk zUG9`Pq}Q)xi^IlBAPK6ga~OS(1&2f=cbZ8%i9{=EH`W@)vy#8e6zm&)_{C?JC-&&u z8b(dB0yrf%^)13YBmb>jR$opfdk7OF&W!_T7_2j6dDU&JI@(`xlNz-~X#Sd28Tp=O|dXAJCke{lAJoURgjrafmC`fpDuEGLDMZ?0#Qkg}G9W@(I zlj_zzJA6Dp6!Yq(5S+3^)5FEtpQjeu205LKM&RqJ{vsTLUeQ+lN7Bca9>*2Op^x;2 z_^*&jX^FqvBEKSam&T*{KEnr&O;k}v<-4F5h1?s2Uxlv}>o!{6Heer!BFNO!XO2;9 zoScB5nQ2ftO91Wl?DjUjBU)_*wtAw~@VP!}*@vRI#W-%^BSH^jQ8B*Frw1}C)Q=jk zw;584Qp5Z{*trl0YjEYDL6jp4NQ(BZwvlVAwZT7xha#7@o9kwUl$26Sgx{d=Aq~{mcs9UaG%chX;^Td zR21MdUE`f@As0n~tvC16qy(oko>D!Tw@TTgG*o^D%%V;6`0YZ_4IG2ReNYFN_6cMb z?TFTMS)&0r)7Tk2e_q8nq@qNf{T1alr0S`TjVJ0xRtBCdqP>|fN(5l4dVHEGlA=nXa~ao@UbVn5X;+nf zp@epk$SR|Kj1(&ZWBN;V|JjRpah^A^+?cO^;o+@m#RhI5`?f$gtG_B?;4wo9DY0#Q zi^!6#k=aUqC_jh6CZn>(D2_gX>d`6UqP>jZiFzh7*Arjlv3~eMLxR`8M=v^ZMJ2^q zIm_KLv^`%eTchWBA$Vssw{?N~Az6Qj`*n1mPxS!XWXG(uX~GY9-Ecf%0(84nJ1?~Y zkeoI?+S|>`tx{$Q)&2%H){94g>3YS`Jx*o0(wfJ<3?;dxDPCz4&;qDPaHgDr6jVU(P-9am@ zk3sDc!ZEeP1%c7CM0!rrn4#3t{!LeBfRaT3HttTuJ%*RLe3Da@?Y>Ja(oVe!Ks+F+$_N!>a7(?Cxd49K*i+S)nJ5I-UIca&q76LhdqzNB9mV{$K5 zrN3cwyl~+z*Y8U*Vac}v*P<)+#UT8ZFW??lfKIA1G)wHeo+y$QAfgZyZ2d1x3m*b z?!om?xToPR`_6i;Mlh~?ZllSAbyLop>(o?%c;prVvbLGP=f18 zQHDlpBzWQXUam)@l2nR`T5qo-lai6=z~%nU8<<)TRzzV zd1Mz?LLjHK!##yiA*w|^#~e5<9R+l^&U;l~9pxn2kFA6+O9CKlz=eaxk%W%yIG3RO ziK++1SwEa@9Vuttuk05t>0uW#VX}?|ey<&0qf`(qei!NX&+)rabp<+%gk+H$m3)Ad zAi?k1BO*H!EZ~n z|9+%R!U)B39EGj_BEkQgpp~cCHG|B(;~Sa;kdP84e(amF^VtR*rMW`Q*gSKM+W!2% zN_VvGRvRZmPlEmk8_^Ag_vZ8qw!pDB_-6RA>g8x}l98!T z`Q=md)sy^`3;D6`JVN>*JN~-jp9dGv82_SFgUj~uK!hq*l;o0y^V8F$)<(~s_7>`X zkS;g097i%A@OCB$*qs3M78jb)-GN(-?5|A;Zoji&+8Auy4!nGKC)ad@R@r<(A82N7 zu7dD?h3bKxQf=_f3)`KriCa5Ve)m6JLkvHpW6gno3~jd^9wuh1hCNE~oTjwq@umbm zgp-K(E8X4mJWxHYH0Sc0Xg}ps{)K}K+{Bu9OC&!zPIdATu>gG6xOtU$AwB%U%Q$!D zy@oN0QH>Ng%XUwpQY4U~V&R=psykCiaho`^+V8WCgXkI;?>Y9?fSGLRj6apPzi+|r z3`QjaK495-Su^WiPdZ||><>OK1cSzC-OS=d_`e>WhHb%LPB%Uu+nBE2=?)=Z%q-3r z4|`F>NxeMPlF;O4HBgI*c0ZpiX5& zq+S%On4&|+|JEct_8ebYIZwIK?A@*fdI~F^NXL{!!%;|xIW(6tDEx|VL&aR+_T9K9 zHB8*M=q4Y?7V4SzF_r5fwDT1!!>?viiDj1D_?!jM(QZu)3T~sieV)MWxRgp1f%uos z5~M7AUrRx=TA69mnZ!S!&nB>75lHv+Oic9V0K$$AMmZxwsQve5USgSik*bF*k-+c5(KbC(_+(02_)znoSpa})xFnau^c*Q5|C z(mDdn83kA^AQG@Y(`0PN$Sp1 z7QQta_pQe*DWMJ^)mTVQNnmH~M5#KgfdNZ~@??uSUXYjLSB<`;X@+%})mLSQA9k03 zXCNw=wbfRz-$NZ+Ytbn~DZE5NL|a1)nyI#wB(VhN&>=#e58)Y4TN3~( zOu?+71p&SdUAfBe2*Y@_hZNoO(JU@k#0^L?T0^aZc0P4dB{>`Ce2809WS zo|`z_8(Kerk+WQvenY~%0buJ6?F_o%uG%n5x9Qn8I)80poPCxuq-82XF^P>ZA{A|K z;bb)!f1s3H>u^D`sm3s5XM3AlsRgeH@_NDb=6<<3#o}8b#!{0~W*yAae5LTQ+L_BH zm*P*36a|rghyBnLI7*x^`GzLKH#+YLQMT)oq#Ee_RbXXj_YdBa1nS;U zg8#p%3iq}hnHJ>3rxBvh$q`56Fd(5C^IJ7c`c~s=9DB|&TtJKu%3G33>vcw$WVpnR zP3D}=*^@i=I8^Zq%zkhHJwU?0b`E$9u#nVjt(2&jGNphbqMRD*$>kclVhhVO4lQo& z2c6Aer}Kpuvdr*erq{70|9o_g0`SmQQ;Fz0(f8hU9DA#bklJgBAki$Ii6-3emNHNE zpnb#q_mANZW11_lV}!Uy8kbiKhabt8zQbTi2g=e>;?^UPf_}f7=e%MqlD_p6oE0A= zMCsK2mh5I=GM-Ly_t8H7^=^ak_ggkV_yV~+vBl<5FA^kgahIzEK30|mJB4{l> z0XK3%iiqD^XpkpFBa~zdIg0Rp0*HHQ+~0K4IKm&zNtQZf9ekV%Sq5j%Rl*0+$$e#^ zM}$$5b{2cfR99hJ>K6QT#f$~Aa#%OQ{Lj?;ObythaH03FDD`1mp{J|-rK?QUi8Z&N zDgc{>n*Jk>IgoEFjfg3w(rm~kJLu*LTjhd5Fl#}94sB&Rt(t!>3>MX(s%mT?MUdB) z`|6w4cret2lj@?i8b2%Ps#|JSLGv>rsRm;&2-F|M0hrvmF;DsL?c2L%@Qw71iJ{p& z?Fgi=gC^=bWS-F|?|yczFykEQQ?ap9$r}mF%|z6#S-VR)iulP5TN7B;bOWeppxN2DRR;(9KzZau->{XV!q`SR!O>5H>!LhRT_V%XDqcqMMD~Glb z+aneF<|l<@i6Vat7yF^gBM!RK13)-piN_*-oOX=t8S9E?ph#P3`ZSH|Sb6JjWbrdz zuku@@bd&}Ywo-Dr+Idy6{zTDwtqU@vHmf-G1PPC_Y#N{4+>@&}FN=ui8wz9VO1S_SH}9( z+a%@W4383E`CNi38Ct(F=V076Z&D{nP#FtS7L_D~n_rpX#+Hmvj-U(*vs9%$8q5pd z=@ygHxH5mL_ImTFm3{QsX5UWR6#P&vod%*rB%}tcg!8c$SDnL?sexs@?Wn;@(^-&4 zl(^3*NFmM~o4=szU7>WYK#?jQ0#xV>Na}1AW?@!ZY6Pd}GR3Vz;3F`s?D+7nhJElE zdGP(_hX=QVn0oca0fMvEF$n~&8JA7EF)5-}b-9lhiWCDMl#@Y?&YnOTaCajv=g^-C zR&)eP*UtI~fGtCNCYjI$?xzv3QWj*S#SDShG$Btw{NF3GQPuM0MHkog_Y%IHJ3Ajm zc8mO%{2hw5N`^gh%Yi;I$^B(?5@_gcUDFpkio=bGC27JucGpKM^7Z;0Wp>Ci7vi|M z)i>tKZpfI~Z$o66*RMY$bTHqO^d+cglcA73Y8;3{gKa2k6C8?$0WZaguIBk6|0Muw z`K5F4FKC@daT^6qodWu&|K|}gPEs6_Bc+rj#TDeL`L8_I0rQiSr*y>WX3M~c@|?G=72_k1Not&aP&xAtRGZsKa69%Rmast8rU}Ydnrk6s6%nGezl{_O~RN*5t7Me_qFzE*voYIk2@Mm!y zMM6&5pE*{TgOeO6t9vu9zj5hs=UFA0o)lP)(+P=6A`GSx_}oY)aqk6gT2sO6cTq?f zH5*aQT@Fev4+t%KF51~0#%9-ewky~;R2yzP@K zR3WsD!k@XBa}aYn@39>kz983ig_WC*a{JNJo^=ub-|+8sB`@Wlg1o+p1G1cx zAx8cpmccG?L!2O@W6#pYr^k@pD8$o2#R8f5YpP9(X~%{&P;UJQV%Z&CI9O}DlNEtb z&szU-^-V0!M{w-;z_6R^-S_;zRr-200H*XW4u z{1U`FRu&*WDc6P^tc7F!?b`YU0~r%ySR}@!s{)9^bPyyh_{9@7UU4?aJloO4E=8D8 zu>kjMJ?dFX884gFX|DBpkO@6?v_EQpj#jLatkuEV_A892zCXdH?dOjkek97;nHY^WTCg7 zvtB=!=vCHY>_C}A{G+6=g;eKsuPG(BRqc$|N<}JX>l#3n@{{0W1=aG{f9U_V^hkH! z9D_A9XT1;_jsTEcY~<&Yt4Z86cEzuKY7Y0nK+$)(xOZPpD{_7kC?=VZIDx>dQhDy& zSY*s1C%Rllvv51qp-ekvensL}gADQ_6_*tH4lCUU@1>((%)Hc+52=o5)Hw3zS>-YzqL#A9Qv@GkTsoq7n=ch6Qa(L6vbQ zA8i|gP|t_vTZnP2vnX_n+(2{$Qrsl`7%v3Of9K%I-F}+6^8l2%97ruVE&QH2UL0jo z;QH?a?4X;i4FRu{>?ncIm^Jpx--hey75bU(#dv^1n@E)eJi7s1kHfA_#HxMTj&x8m1w=%KiHuZdN=G3k09GfalMU8n24EG>YDQ zA5mt+V-2=H~-BKSTW_&jIbZ^e@kl?s^_#rB6BjF1QrLQSd<=Z1LZh zKs7;P>u#=c>X#b|I({k~upxgKdeHcOT<+dOgp9@pK4XO12 zT%7Yn!RzhvIW(0%f_q~XYXk?ODbyJ@wi=6+aUkj@}78VY5^ z*kGI!_R`l=Qr`Q;N}t$K%b#44x?le_N}W%7nzM}99()nw?u#P^A|f|kT0=ZHi5DSX zX>6Zu)5+Nf?ka+dOSL`_D_0uNu|(gsCoEyG&JPa1Gn~I55T*JG$_=T~4G9uqm}=Ll z<^|E`4>@(hxPZJPkqyTi06`*X5G`EBq=5WNWh9Agssr#DBOLw=TslCYC!G zL;_Q7fJ}ZZ4Pq&8TYigX*9qOq5Kh8<){J9}#)dmmH7{5Fd3ewp{znIU6nzPtpx`n) z&~^+VCz8mXYkf2#&LwgfnI72%!cY|w^`Y^AoteijQH-vTaMp*!3%N~477fRU96?&T z;hhmeif!P?<$I;xLKpMNrx zQ6l`1$|Le{?mHUKG!1!t4}#_oZ$0bo-4_I|O{sZa^BkY3VDXG1ci^d5P$^hR3%2u_s0-zGZ3}GvML6|-0P8m7FH;WTU zl5m~%XvR&V?k1=wrNEiFoPxwC93hRLqznfw+{eAcx}M#c<3UPeP+LArsplP7{5%~^ zEDRG=0a1O5KZuaf5k(E!YI5!5@io85wskr!%d-Li1 zbw6~xzwP$^7EouNHjIhx|2+1|=@1;HkE(ZOwS|5`d{|(!!Dbo^3ubJ)mG>_5?q6kO~wN)v74U?GhoQN$zSOAw(cI1xx zMS;k86xYM_0@+rw3E8mA;-kRG&i5DRy1_5e{AjHOh@oH)9nT~qVgX!OVu3v+Zdmhq$5;jWHbtjb-Gv5RB3=lo;F9!tLyA72!0Z4{ z@|}iv%cAgvEh#QOkiXqVCe=~@;l5alsBT9~K4`kW>IuC) zA5_kCvB&Un-EvY9+5Hd8*w1$$pZxBQvH(-v9q_Ld3f zm^27vbGjhJjB%1x3M4nB{1d89nb))!H8j`nxuKI5wy!cS z;opq#Oj1e=iiN~lo*oZWk!V7`v8`6_m?t9Yu}?=s<3;rsI6HeOeC5>Q8QB&4RylI{ zSP~zD#!sUZKS+TIY)a-vWG%ULvUXMR^ie^|Os0mNbUv&GW_C&Us-4DL&HTSQCPydA zQO671OO}Eeoe`EE0Qo3f^z80;Br>H=IP=Tk+qYHYd`Hnz$9(@q&5V9jf2z|U=GD{& zVLK^Yfhtg`3tdVC6AzLB_A)tM*m*THcibAd=F$Rq+Q}&#lljGS(1T~k3H8@nWqQ3} zO7}gIOo$|WKHz+2Lt{uIarTU0)>=2GW=UA(gl7A!^d*&l#guW%&JGIM62)y(`q(I; z-+z9P3=#oD$j{$v62F}R73?N6bAz}EYdxiYB2dXZl@8$EVu8cY<>@6hYS`fGF!lM3 z%{vUj7c&c~E^J$kJVj0w+FADjNSdyx+Ik%Ha;=&nF$S7lSZ5eiI&=J_S|u&8lP~22qc=`d(63fHZrqzgT;x6~vL2PDYV-G&|xp z;`+B7W8`|Oca-p*Wmdm`l*1tigQ?{AdV^*(LmiCJtQ|^@uBuA@jwzmvs9Mq@CDJP&uSzUhPiRI(xS-&>n^Ohf0+om ziq292$)uBQtFud}_IO!Ybp;vl&VVt<`TzefGY}w47Wz)l94U_Y4Y@V;t7NA}R@lr9 za&bVI1SLfLcM4Bvk)WSr^%T>M#!+*gnSySJk$7~I{o=Uu@E zD0c&m1eb{!Tw(Qw(g6yc%tI z6v;f(WNR|ue3E$+$egtu3oC z)D4E`UnnB_3kL#@yI>+ojmmM@P&Zuh2^5aA6irfY;h#|*WGMAi@K$Y(thNp;9;7jF z8DjO8vFrqiOvTrtkc-lq&~oqzq4NjFIdW2}JKO9mU>?APf_jxM-D9GEB&9rHiJ~rk zK}R>HImbJyn}%ve-HWaiOS4TZ9!W54=Njc!Tm;t$iyIOJxE1c@i^UjMZc=GNlT;b- zy-}e$DJ`sqMIB@y-T7=N(}&s)CNt0Oqyo}(j@p$~Mf~Kmr+-PS7@|&oQ+7y>1lYSr zN(1DTb=4WKsgYt`y|D5j3lA13IZ-c9sA1xF>OI@PbQT=nXTYeQ*goW?ZvRU{&H+6v zdb_$`$P=D{_I=^X`7Gsl6DUFAYc^n|`XJI0V!jNf`<~|?!L|!>xKHSU3S)`DPeg|P zC_-^P?K6CP?47sUSU%fH(O*&d8n%=u_@m+?FN!JVs&OLw(`M2QbK4we5S z3CyE76LiYzW(_(ZXjG;?o3Z1~AP9!*^NYTe|4r}&(Rdl&dI#LETq_nVBg)3WG> z@S*Kb81rh0nO~+;M0Lb)q9AHN&U9<8iG5~l)M|*`Z1(g8zR;j(cRW}BESB3Pw(Q3j zK!mpwtXBTr;L@zo#mzBq3GX%8JrBoSd&!K%p+N6ZZJ&CE0>K4bQA*Zc!ye0^;R$86 z`r+4%TH(u9p=Ww8@5j>*EqGaMTPqU|Cl*`;uvvmZf=QVq>qXOsk>nO7W|} z0hHv_Dr1_^PniKwflMhGC!DgAH6>)dTtmB9W-Vz<;xZ3=>m}n!r|tr&j4NnH^tKti z6u+8PSOimQoHEGp6VpGNs{0y8-w$|N)FErc=DVkI8RrT4>==le9HO7HVg_CafEd?Ybb=d8N1i+&6cEBF*j zSr$*AN*0Z%MDUuog($;F+xxhqK&qo>#hQd$4;sEA^mT@KriT){tvw-=6HdTx!UBu8 z^oa52OZGbdJ@V~{8BB*yGG0^jJAQ#7=)(2l zQu)|9q?G1rwQoK4`bl@#6HvDhSGow?w zYla_^Qv5Sm>QT^o8YNDuhwp^|t`3hWat=Dt_Uxp2;{fbcR=5%VUx3c9PjYYmTvBS( z1K_y{lGUHlFO;Jk*4jyN8b@V&wt)ssf(wqRGh^lgEqLu?dJAk~Y6EnO@(f7SoC_;o ziNhZYD>Q#pSZW{wG6L1d@hKhwTk;iy-UwB29h_5ug?;zflY1L`{7p^L9vA;f!92JY0iv{4aMfAJpH@VlZZcRUk4@QAaV>MeW^{nS&HG~)?`8m%OZ+=iYG zd4Yr5HpX!sbl23;d))t3eoBJX7fZruQ@q)ObRh@oIquSSu_Ba#867SymS{+=L3K?$ z(p_hNN#D(`Rf|xw+|p`(PS^Ok5aDN#bw`6+XULz@{3vWGk|7w*2bwuowB!vq2`oYT zEz*@26?>h@LgEl{f7Kc9c=Njr^tFXEL4ItOtU%At@l+fie~C%<`p>yp8t{CL*sE=u;6=WwDRJp4tuYWLqP+pQbb=cJw%r zG|;%JhhVvR(qFyhJw8^InzR(G;4Mxk>l%36nnEK4mWCz zF!Q!ZGZ!I+q??D}5?H1xkTddem-Ismi}KM8wY4NLZ&YEp3nRZs^*;nWfc2k1D%N%% zW)X*UV*A3%nBWQ4na(OM@?^*dO(d>nh~%jrPM5HO4HM(9lc!qnx35^VGz)~-vtv=M z#_r72(%VY1swiOD&$fznz|mhYf|$j7@J|!EJtBTb^=O#Fg7eWfjRKaXV0-ud?UU>? zm~`@XDMCs~#FSo^ct6KZw2fRs?LzC!eJSM$(1(jik{ZexZ>aL^(UsJaIU1>O(W}M)0 zSYdEv@dlfe93#glCoJUX7A5Et3P6IHIi428tKN#SiN~bV`{{JLbz&XxisLP4EY!Wj z=qM`+By1E1;G8cI{+wjjs6t%w=g9nZcfJwkxl|sSsUikiSxMNH0EqCX%r8s&Jez(L zYEQVb2eWPSKisu3xI zwqOjxxOAO|u-lllgpsigLNAnZHY(Xu%Rx}S$q7Hkrz_#v-$D(}jcn<7X)ebanM+kvOF*<|bjYZVK3BHl8sAi$?qW+qmjuR~t-)a3G=}k8bYCYV;yoAVgiODhpmcdS8;3lTEt*%ppM&AFp-*s$mK(qMMrDp**H20K~!h@Hqy9-bE{<{8dA0<4YgYZXD?lSN< z>bxy-1Im|Aq11e>oIrygC31vRDMLAG!Nz2VARn?s@EDA6VeL&0peZojBd#Nu`v=ZhAcjAzdvQnEu#!y1n? znyOQ+NaBKSD;&U-H$5ws0|F_J_ZFtnxGT8}(9<&IX8Si#_p7I0k*o=K$zaB7Qa!~} z8H}?=D^c+K+zApEJ1-eyxp|%ljfQjpIJD1z zD9-mf9O7;e+x3a#U4qpuuI_z`7s;^JHjDh|5@Qp@cU4IpRp$u?iKTBnur;^UD@WeS z1roNnfv5$ZjBd#t=S`!?$fC#2DJCJREzp24=4HP*e&Bn;y3a|imik+%%^4nJH>q+8c8=^(U5a;RktH*U8R1nf$HMpqu+h#1`Je{aQlB8Xa3@9v^TA z>y5=zi-*O~{)~};l?D)t>rX4&D4o5sRUszImuwEx7-YGX4XfCeijy=>O$=H;BLvoK z*r-&`?^F+|v4Ebt zY^8c%vHpGG)|9GhP*oW#f+|wTyEk9_YnhQe;$>3I2TSEC!3v7B4KLshtKQHG;srkn zn3WD4_!Q*#)j3a0pe@)N`}N3+nT=ZC?lH{l@;D>}yv4v^SY8Icia7vEIDYy)jMpA} z+=K&vpZ9S4>?9IxY!s80tXW1f6^r8oZR)#cPY^YM1UnJa5I0n42-f0C{aNfHQ@Gki z;ED;{CAhw8MU_eDAF(Y5v~tI9?HrjTTmo zql+7dMahfO_ZTTB2c+5NprIn$oFZocEE#02pqxgCLCA^4Ie_vjVC|(5lKOV7l*}_F zePYqdfB8&wC6ce2j@fC;UPUIR25gO4!ZJXt!GLUhxD7Sys@NJ+S+As_PcChdqCZ+w zC$6zL{bIk6R_J#;x|mc|#YAH&NL38;FCgO>s^fDcuac{K_Fl!$17>Yv2q$wMNqDvG zog9vh*F*ZJ?C*aO0|k#8#VW>KAzbt#*o{NJEPq78J1Y871Qe`1oNKj#rr~8SrNHy) zu><)lJZ@H|@vK`C4eWy7ep6>s;A+GS03r92)c%_eEGSMR?ldo?Aju|`K zYS>Ta=eIJ+N4f$b1LK)r$SvH%Qx{&7$7U(GZQ7Edape*T>Ta)$JL50L9v4iptaWOs z5+C6WM2|pVpqA-`$8Y7{u5+IxEn(y$as53vxi_cAOMfv=Ck&mG0IEKGMeugLnGaps zQ}Ksj07B*fQ-;Fq!*YR_DLo8pW-c1fn=9OMC^$lJtwb|(_`3~Gn6d?;qs-g#{ucfb z=uwCQiJ196GR6*RwJ&@kU6uSR57NVB$JsK~8ihEX7R*b~GIzm=O)r!+W!p{@KTIq} zVn?~g$3mje>i*oCbbH_7Zb~~(FA%vvX1cfK*FBp0>&XzET&GSqwPFYx;;+j*ZaNoH zVhf#L^{7jeAeT`|G@TcbxD7f!!d+mxLBnZtA8-DyWD?)BLLiPg>QRyCl%Kaw2ICP? z&8noWExXC=(qxM02wCRq?6c)#C8X4GU@X$?rOSv&JlntZ6o^(#!()n!osSf4wUk)Q9&$|Ufs#|V7|DO(*T>zZ<0mWV zZH+;DMuoeuY&%m^G^Czq{wE*&f;s+hfM7B-Laii!SE(b%NCq13bM~>HIX!=T`}&=C zBLl34Oz>r8E1QiI1$7-j%&``%Ok0Z87J=@stk(Z=HlVHp=~ItMF-yzy6Akgq<*8M# zEFCGW1|v^%8JY`p%@DnAFJ!^}Jy9qG1rTl4| z0Zo>$6+ffE)6(Na8={VVFZ072zI2}3K@xJO|MTGdQb_B^_*)EYPOirRf+y|-aUZ63 ziVt-7Q0#>OSc30Z6lORFa9KL)y$9kA%v}7OPJ*o;t1RBdevO_ZjTc_D`6l_*D>|nJ zghj`+$nj|`32d~o_G2I~qE8ef zQQ&D1j~W=6V=8F`FUeCMdoOQ}j1&BZ0#a3=8`kxB(m@WwBUnqZ_ZiZLz?S)6r%j5S zV^*pA;NWmZ6bx#&iJ;4t{-BZJ8e=bZwi+96aEyqHCZO)UMsp($qGbcxEGEpe9D;Tq znXh`Cof#+k(#rH7V~RF#$hNMS(w&nW|a{YhQMSeKW69^_nBrYH36VhZP*g$YlyoIM1YSoS~AmPLc36~SFl(Ddey z!;2+WtHRxliC8_luM9f=LoqV32!@KHq>$mYT_TJ52Bls~<00Xg4&Ym0Kj2;yZg>at zJ5jLZ7(8NnqtY2x`pN9wA_7B$)qO#4CeWIw8zXgdykO_Xh^XsU67g z?uCYG;J3W9SzNvdKaGb+8(qay5x3|27?iKoK?ncu_Ls{(Z80V=-48i)(r)Z_vXLXk+!Mlh7cAj$3(dC&B_+Z30X*Q<_P z0+2?FB_avFMIU7HlDdy$KZO(u{LJ@PKr>g)P8Kt+YI5U&4!{~aB~Hp$T?6M{@X})I z9=k*?*JQm=0pd$oI{7sLA-f>l5$DNMm1%VD-tMp9mGIV{hy(MmKbSxqj;I~OH;@zQ z=?rOWpYq-yveN=I$wqd&RP?qS%!QVupFlX-GjtU4C)#|nu1jXc%LLOAuFVC7KgHI! zY@{oZW1!()lSAe)ICn`~~^*(2F7n3hE zW^7Nk$L|sJu+vgD=MX@S5VtknKX7`<>r@8+BuH{W*|qL7XqB|p`ReCJXnJ3fL?tIR zFwtBkcAlKb&Dcg?6D8vfk(-#o|4c@E$B=M3gBeMJu$S==OD^Yh0TZ9CByTY5lEXVd z0evfXLv35=5^^cnf$Upzhx|^EL~AWTuv&~_AS|AhT3y4sjq;;g-&kuUlhl6Xk%&D4 zw60s=%XgP=o?2Hag2!U7OhU58TvbD`v?CG1H&In7xLhOdCJnpz}xxwed<#YF0)_f;d z)5}VKfMGAv!q4#AoeX`m%Gz8z{T7NZ@q#vyw9ES0V2+8Z`^M;;bmSTzkbARs=(k>* z_$X&p5H@%<&^Vsd!ET(650tdLu*fD90ygdDK2|hJHnq{B!M$fis@G% zMG>DAtB|4pzWT;JcoEc_7=9P*gB#Hm23IjEoibjM=Gj8_>1!iWl>r+X+?XBGv`(jh zp2g8Ml$`D(5n58uppxerD|hMZ9r~-Tm|Rdjj)Eo7^=EWf2r3@MmYQ^WhO1BohxUBD zE3bmtFcg`=3Sr@o&EZrZW?;JU4n_$4&4Mw+oJugf&xW~%2w08+I{_4K4fZEZq}&U} zg_vlZ(Ii)QLYi#+%jAiK#KHtoa9wvgbeUgd+jxN8uI=;&pW_W4%lY-Rj;?Dy9_Ms~ z(u@5!P(=)uJ{NwxgOD7y&qGpLX2xbIQM<`GTL-nT!8mx}VEJag%DxHrX7X!y{a}1C z$s=~M(NN@uS}d)P2tYnK6r*lO&fBhVFIwJLt)K0{4`A#r0zRjUmUaKWp;mvzY1brM zC{kq{`6_hUFso`kI$-L%fg`p3pT~HLJGlh0Ax+z9^^iC-P^j1n8k;HRe*@;0D4D}` zw!hbwM~FAxn01I&-!~*y113YO^Usv4qQGzu@t>NDf_yk--9Ir(!tA?evI7#oZ+96p z&dE~!X+Y?$>&seYi}TO&Fg3tpL@#$<%P1bl4tjGnpv3A(333W0lAQpbZ0pWJV!9B!YuK0}KbIc8`If&>{$FJ7Otu4f@zl|;J)I+GP+D|~=4hcUTQ3<%0ZCND zbgWz6YU@IM5Y!osq>8gAeRlpm%r@L?cH(2}1*XhCu%z<9!Mbg%J1Ya5^bv z02ykt#mYk`aF0VoV?doW1nG?@AgRcb0{Lv|k@2m05SCaZvW=27R(URzbXK{DQUrZy zmM&e4t;eqsOe2f*uVy6kYCGCcG9~ZQsHOy)G}gTFK-Lh_{I~%eg+{$3VT@$K^yPh#o>6;h^2(rU=AMzdzY-BII&$Z+X?CD_;u!o+heMKI4E7p`Ijfl3SN)62 ziKtc(>Zc^G;y7C-A5OjHXJxB#On^E~bhuKVL+apdm4SC0mJOr#!Y>bUn?o9;Fr^ue ziK_MDOua(}?&~{y$;IC`&#O6-Ze4EExeiqOEd7Fu!L8SzAPQcdcCMtjIuxW*nRUE{ z<$Iiahi8Y1B8}Pq{_Tx=+53x$)w5R?+r*DMU9Ori6Ywk}e<&SW#W7 zEV$w(s$oCT2#HW?l1apLkx_WHrhpar#T%~z;l=+Q z0Teft^t9b~zYtVWra>TVqmc|i;X?yKap3NS!zrxug6U2vf`<4fb)SF+7*KQZ7z*Kh zT<9Kr$)>xkaZXa%y(Ny$O2|QWYMmVSj+DmI>fJzj!)x1-zr(zg>M_^D^}~YY%A`+V z7Y^f)!$GsdCGft@_ZCm&vO;xRpy89SL*jYe~7{5D_w*uiX2z zV&5{Y5Zdl&s-E$>hr#L%c38;Bp$HYHnl)U4aA__*T}p&MfLhhlmmG45I$7FWvaWBm*;q^kHBQ9MLS#$dqVInIPESp zF{)m?*#$6YszV;qAI>2Qm#*gX-2_=i)f-;DbVkRfoh0Q{TI7WX+!!6H65Ob6#!z=fEN?wNC8 z*5_&rWkiJ-@&(E;y_Ch0g)uEMSi{0=bLhMF6ViVm@VF;9FvHSBb@{P+!`&@6kld)~ zAY_c^{I~0cVVm*M!IY7L06yRFJ@{7Se6A4{QoFp|xhfO~fapOZrG}LFLSwc{u-G?V z*IwTCjpsMv`p6n{AFq&)by0ru4T0@oAC{7P?+KoroORiTV(VC_ZkYTPXBERx8)y0R zy8#)0yU*jOlfg_#0Z;?g&^!M%#AUl|^?x`wa!-M@S?OqFpiHmsef-PCkC1gD1q}n zXtrh`QjX|JgnCoVRrHJ}`uxMu8L%EG7^y7?WN}fOeP=fmoGesxa`w@FEY>9pml}~@ zn;S}59;AOV2Zc%lCE3VmgFUxj%q~so^IF|Ht$2Tm`V#%`B~SE&biNkBXB}3o z3fcEQ3NF^yx!LO4?_dC-cpWrh(@yN2fs3L;oWbxGuEN7eLeD#&lIK?Z zi&uQ$e8##3Ev$()_;G%_x;yN^viyC7B;D6GNY!r*nSHlaqK*KTtpl^1%d!is5A!`ftyIr{vN0a#u=3%)_dt}E( znQFYB--YPDD?r3o`%bfsP)hS0d9Y>dh8b`Qa7xFhRQ(J?*_iUm2p>?_g?9#?(J^#S zX4{fab|TBx_t|OeoUARR2Hho{`X`MY(U?(D`;iv%2H+ zo;O5~&7!e<=|;=$L(k<;O<$;oTwGU!WwEen9o2yK5-aq`nrY<6uve;EuN9vx`Wq8f zNt{YyvIr#78iZ^~={$wUZZTHqV|_<)i^NN+IiJF zY%BW|XkJRfQsn4;9gHX$M3$IgDGV}zf?;?F6D?UT4m+!1K?#qZg}~|Nv!;^oehA%m z(zwCojLy`~4ZM_&KDw_c=8(n{c+dzdt~t4Q$dgrXE?7Kyv%Ws}F~SZIYIHD|qYJV!vEVJHGlKqk~E2));CFRlk zXlAfvmZeQ>DY}k81IyQO610-XEO|r+Ep{SLKrMDDI#GHmu}mZ^Pt$Aq6_n(T3J|&Ct)5LN!T^l zGJ+%2hoTeSFYqvZ;C)@)rF`=hN_mF@&Y{Xp!c+ml4IueOCa@++RYm=|4$AmYjpfU0 zkoKR?a`s~`U<4EOTRzn@NyI78jRsnwL#ePuUM3zACl$y>bqCk+sco~&KP{thCmg9- zB5`Z*c0aQP9Wi%|`Xycgf^Uj94pZjVbaj0=K)630*!c-(jK2E#uC;C>k}1Ww>0*37 ze$xyERl}s2(!^=iq@Vjy2058sa^Fi&{P1z2X`ylGxU_z#&AoJZnXMFcQkp{-NNc*ID5 zl#O3}$&yTUW%vH6uY;4y6uIzOVFNpdY^^>E;vt>PRj*FQaLUFUWz}2YBw;KFGA`tv zK!e?UUz?R%P#w*~Ki=-I8JgVtBf zn9dtXH$F>%$V4ox!1Pc`=nB;(N7OtY{|@->n&&sqtmnM$P&xw2PbpWbe;?+Cb$gSQ z6PlUGSnhiYjSqIcAm z3AhZt<03|P=5*~Oxv0CE?(>r7b?8nuxgSxxXk-@xkIBc@nu`I;5{MUjn8K#OXpNoM zKA(!Qj8W~rtYqZYIy?@T^;TCub9*|AR26^_ibkscwQJr?#x28c75y-(_Ld((h(cci zOx3FszK;{qCkD0OK|-Py^S*Yw`Kc4)Z@I{-sKyuZg;SWAPYkII`Yj>beP zUlnU#?bN<1uBgHxc>jDuD@b_|fm(f3?u>X^&v>S!DT+}EBJEvIi8?5tO*R?DE zx0YG)ucr&In^BZQij$6`j+@o5PJ$k|SGxBM)#wd~Xi7 zFHZyg3s?_RlSd6xb8^@%m09ma8{b#-%1*Epb=}F*nwY$hYY>7lrhFRh<*nr_;?&K| zoJRXE6KP3At`%KVNYa#$7mPd(A}y)$Bp+Tu&bB}ilZy*1a<1_jL? zT7PS%aghs4iNkspy{ih+Dvq`2QsQ2o9m@N4ik|2CNZ>h`B7_In&yVJoyogDn5P zjp}4qPBU7uJxfK+uF^Ifj>8Hjb`We@s7!Hg_>@d3s`C=rKCpqqC(ayK|3qgyHWbfv zR4sBxLI_JK0$lS&hMdj$LRtHJSj|f(yBv9S@Yg z)qPsLq-hmFSMVO}r>e`)PbkHez}oi&?mSeedfOn^9>WD3)X6-^+z-8HV{^!FZh~g% zZx`4T$La&w!m-_GELg@df%P^bUbYwY(XX(X-Xf%IFJcwJ;*KOMe(OXYj~Ug|phq^0 zttK+`RZR@6EvWmZ-fW)u>Dn5{#H^?iCE~YdsZH69=7tbpk^8OaN;W5S*q0{G6r4u_z(hDee}p zN;?6>`nRLJ>0ulSTW3hOL_I`tkWp{Nd5;bCh(v%zrRQl8iV;hEM>(Cu12#O>or?2^ zw|g6TXrd5Us;vc)nba;}&IQPed!m9mStc||To{=uAX}!XUL(%4v0jTW1y&j>#I2ln zda5pKnEPAMi*j&VHDjn}Y)4ISzaVV(mqPwW7&__-Q!M52y!h6a6nj-=OkR{d=rjv%@J0UJ~H)oL1V?PO?!13~n2=Prvem{4J{E}DsygczDH zqmB)O(KeGUID6{e0$!9#GtMpZYpxo`4NO<<8twRbrpA3F$&3;#FA2{2k_mA*wGB+3 zfaX1Xh2YtPe{Gj4D?=bfoZoTOexZ&BYXT4xs1rFBoC1wc016SbYi(Eee`R0Uwe6x| zsDhRyxjODbkN^2GU;`}d$Y;vn?^Bfbd;d!j`So2gDtntSu%(2iWhmr1FNgxv^UeS` zw2506L*n<50kq-katnk;+ffDgx0r)8pO=saHlEI`tfd07S0h$9oT->8|9KKYf}_jx z_SoY9N;%@HHkFj^z{FIDXr1{hRRiP`%p|7NK}Bm_2DX$*WEZ>|w|gMLc6UL!7nW4j zE9`Vqs(StB7kJ5GBu^11q#j}ny(IU%AUy7jUDY|qYu1BXccj1vG~`Ju4THF3*x!=| z*<|}P69w1FYK4|gal6maWN}GRg+X0sS94p@G^`M0K9X9Kg*^`o6!NrExiIlOD-xF> zC;n_yJwVXyZr3I7vV&h^1ve#0mZ5z_?hpt@5?u1j|T5}kePMnK!*_lWS1 z*slDLi4uB@`D`kl*g$#z>c-hmoX;g) zaqJ1Ll>tmyMYyyS5vLxL8ecmmf|}KNWNCb>Oc z$R>EBZWkoahTu1<;Ej?UR_ZvEe!_gaMQx{-^=>a@dkRfT4g^_S!KJ{pFYF)&2^FL` zu%|%7DoB|glMk9aU};c4v>OgTFA%_n3z|c07m(>~4Sb;hV6=z>w}X+zaJiK0`rzysS-cc2pyi|qq zcMt_yBzaD_2bD5XVA>4-zr$E_o}s*c8PQ%MZ1UO#$@8m&_%rG`Y3~3Mvb3D8ks@3g z;PWiUdcKg9lfdJPpSe;7+1w9lEQw?OCjWbsi9w5BpEf_+=_woeB48vk3x;8v^5r9y zY~+Yft6)WkvHpf6``IQ|4AYe+9kfpa4bj?y-6a}oZ%dgw%xIubt|0ZYh9TQ3)DaNj zhxo~xWehS~se)vx+>yOvsJHJ=t+BQQ=443u+h-hHANdgCkygVBm4V=f0G~F^uqCo` zq_yQHch^8~!sm%SW>8K2qIV91t!&a6i}x?d)MU6ONL?`G^>2HQ|5sOoc>=2YHka8A zPRA8R=Z!s-Q?(hvIwx-P#GnUcA>FzT_c#|JXPZ%02xGk(KI+($kFv=KgPG#}U0> z3XRdqo~#!j8|+et-V$P708&Z`5`E>9+&VNdauUwQ;92@F!I~J1D4=WI( z(l4c61a|m~;`RE6C?K-8B~}J?_C^Oi@l?uH1114I*x>QVx;)xH(!{dvpFP|U?ujM< z_`aJc92FR)PupviAtL6*!*mUXfOsZ>l5Q61gyMP|uub_rmz z2}_f$s_1i4LsqDs34A9oizYMpt5V&pr!$rjMNMi@RKWVj95_uMB7Gl=iEMBEkhq=n zW<`}$3>oJdI+!&bv^0p3(%2x8MIs$0b4%o$gKqEwW6Op3S4{xBNDe#EDUNlj!M$?O zdNnS?c(=NYdxTrGH0_nq&It57-t~8PKrfNa%o*6g{fdr z^EVQ7AbiQKU!%7WCf^=$QenEhxC`y+DBf+yv12Vbhfu1@-qcVLXV%XaLeoE9j-{24 z92Egl!&*Rxyq-fSqUol3)*P}X?u%PeSB3(^<_}GbXrbs|AEqhh`};v=R~&IZQZ*na z5a1t5a({1<576mpv@I+xoi-xVM``hTCLQHON&t3N%vNiFb0;L%1EVmz;IZ)D-?FM*ivJ0?;A^a z)FI{p8Dad(J8Qs`G%>nyfYjDZ7}$5_I=T5bWd=U#`2Ew;-<@h<01~6=L65ijHnyvV zKF{Gb8`8W{Pa=?u#_3%#!i_<#w9EBBx+9V2dsFs|UE#dz%oQU2-?g_c861EZACkcv zSULz%h)TuSiA_2)Q#Uj@XTuxxAZ8V0?XB8FVs92$`xwCu@CfVk4upWR`O$GeHQ>H7 zknC~?c+ore?@*mNBCkks>5hVnT<*o2!>aXD z?D{Vy5KM0TeIBz1R#I>di?~ZF3Kus_s&JQDg0D6=cycP& zQ57%)JXY*r66r}g``N;CZboP6a z4cyD88RK2vdEO{)tx?Wo4-988{|Cnl&vaXoRttpPAK@&>Denj|)FLIlf{47x@7_^Y z7zcU}(+k8+DnIi$6^;`5hS*M*@OT&G>=DEL;)o}NK_~ecJSHYwdhmIpqcRUNpM?G+ zlxSCQ<^m9v7~|nPb-=Gb1m}j@Bz?m(6H}0iV%%_-vZe@jD!se~3-%}0d=vCp8@`UZ zmdug}&^^m-x5-(x3wcLILxkpN{Eu8dk;fU>o1~z(-Jg`KJGDK`8q*ANkg$OPV>c4D zq75cNo?$~6GEE+s!<{|4c*`$k2y?&j8I(m`8moey(zBpTktsDIZuFC6!siaQ>DeOw z&`o#|(RT|2x{L`lVI>dj-!Ls4@Gu~PWGdtSKmd|Ru%qZtQBd-a!hdD`JK%4zo-meY zg@;V@Hfgn4j_u*){jy|RMasV+A4IAao+=sFVU=YNSm4B-m+8ICF`{$C2=Q*0+mVNf zvj|2r-Lp%+NT?p>*O3_1Ara5Ap_nZiDV4i6xmPfd)XWu~?C>wjwnWX$O1J6qetS-% zs4IIbLE<=QKUXGnrb_v(?Axsfk{D-1iC{R%NgddnD2qj|Y?SCJd`tFT7VJ9NB;<)} zEh*-7h@rkSiy9)P-2GpeP9M#lDm(H?B&8|X-Q*|Ok7%VFKm4OXph&*6Nznu!0iJN{90c8;hd^HNm-m~ ztk45nswktS3eV3ayH3VpX&jqSpLOdZ`AhQMvB(2sy}XWpotdZWLsv~wgnA6ZEZiTW zH@r(^Wc5xBiiuC-C5eTTB%!|#rgQ{cP54~DY9v+7ZXfI5P~NXB?18CM^>k8_b*@q) zX=I+zJxF{nyq1L{B{t=&|C9WGZ#&KKlQ zh@*W{|J;io`4apr&blOKj(F#^M1}rM?_T`oHVV=eX>tK1hDk!@%&hi(7Wv?8;s!7l zg_x2tP^sHK4|6q-7^P*o$?Kiu_=LeX*BqOro*me{J*V_Qc$2H&qsbQBFa7f zcdpO*qw53}Eh^>_W|U6+Jn?J2H~L>Ku`fLy9jr%RZnT}7t=MRAWDD3(NGRw^Z2O}G zMWmyCQXg>QtHw+t=E=$?(~d;5X`n$u;jx+6T_bO~g*o*h{BGorOa}uAOw#mN4wv?> zO6f1DS>}=*!fTyDLViNL6NR$rRgwN+2F7SCUN>gVM329XNg5^$D?Tabzdt|r^e((? z@$#rtM!M&-YiCA%cReb06W@Xu7yiW)$`-Qe*uUva<6fKEZVx=_NgRLP=X3xZ^_AC< zcY*djHA#n9e95rmR=`@D3Z5tY=rV$Qp0=hV?t5OO4hHg#(n8}O1?J^5U5ogLaC{2j zTE!KnTk?1*_{Fqb+uLROjzKlnlV6m~-yfr`%Om$6E0yxssA{QzoOClLZ=vKdo&IU+ zC*t_N{n4Ktk1DBlgjIH$RM%SHe^N=ReZC2}XUxn_*Ks-EZoaC{rT>6(Nf@|BmL)5O z@AlCX^GQ11&`RO^bvXL!TYaRpDG}Gw?jFqPZ(q(%21-Tvd7fMTY9Sq}$vtlVFjQ^8 z;fNdx-!#8lLqKzCPiowA)iM7SL$uP@p0Y<6$Xq& z=r6vntf0>D3(~w3JGpRRvo$f&3|3R2AGc@IB?w}1)($W&osjRq6h!s2W{!pSjXa2P zGe7P8m<06aTl9PBsq7GqRmLC+Q&C9zt zkr56QhuL!owJ9N=hot9qW9@n5(ez1lETM(x7pqOd=V-Fk=<*MuXl+a{nyVs)+FEdGWfu~( zM|HjzRRSj%XIVJFaoKi2`)byp))^;vlQ+=~}S7$*X zQ(0xO#BC2>Ro<5^Jtsk{B}LYzv@VS0)g~>2W`Tf}xoylE)Sei+n*_J55>$AI41WqU z7|c}brii=1vWtbEu3P?{i3snZgt-5%ewCh`(v29+kGvkT*~Mp&mRSnUir2pP9x+(o ztNB0kUA!aVhzqF3^3-I?6Q$88)+Qa@;qfvgdKk)tuz88HJ3@H`Ge2J4UK;pEt;lt%lz}Hj>?WBW3tWL_n zO%xH7iM#tnQ4z?EKj>`z;h8%0NN3?~$v^ zZk;YjuU>rSE;{j#rD_GSMt-y&GnxyZfrm&~_Eqb!Jka-Ca~Af37FTOF8Q_`7emKkx zx&m!_mTW*}915%fwpcT>MYa!4X6$J1z8e^_8#Ukuy2&h_WhwOT_`eM_bQjhL;5bm7 zg{CHdGa5d0^)qqInB#bKaOfCPx-e(maj#@iSIDoR>;OY{!0Ycl)ur7wFU1{Uz59)Y2$0}qowNR@_LKceeSk3s*?wrx`gL_wMPX;F%~jM}2U${8 z=S|3U-_leR!^s~48SyfTDQ{a8pL;mo%AGp)G_}n)EZkuV-+lj+-=DJaI9n-HrqP9v zWMUTpo!;WomSRRFMgqK&#nWxUR(cS@y@2Vvl_IULwca$+s1J+2(h2eeL8fPY>JnHd zzHmTMBzp&#i0aD8|ASwwj;5%NFb3qJOZ^A88Okp!nahNdBZmm;cO7h^5U=&&@HyAU z4!C&bt-Hy`KFwM(_Ve<=1sNRaq>|AL|No>@^$$HJJ@ofhi3#_?)2o(fp{bN;7OV3q zt{AX~JAo1t)Fb~;5(kKC5WW4}NYumPO`(f*CG1dC@A?R@-}v=$Oy^hYjJLxcV_av@ zN;T(73^|c%q_^&Jp`^24Ie!&fk{vEcv6-x&HC(NtRBpwmOz^6jsh6{sFgv3nS3}#9 z8Z`z}wgc>kScpFfN?Lj@pwP{!lw1)SvAwWo-B8NGuKpxw$yN6Vg@3L+H z3zL!wfWqA2%Ui7G)6NJEJB_((NiqF#4j|$YDy~d(&Z+8ZRH+)uN9cT?JM3dHIMd7XmE|zxsKz3mOGQ1(`V%yf;rQM99^D{x%!rn367sHDFMjtCPulCLp#mY=*{kpdg~bVWFqC}vNGr3g6t2C z)+KIq1kDFUOq#ZvtnI8}hx5ipO&-x}Bet09-)<=Qmcrc9}L8rKFgmo5M~H};u%NK{iG^2ROWIFNR`ldE}XdzGRsf|@bA>>G$wIjVjf+*|*%^LPf2+a_HH0giJq_~T{1|D^*NJ>% zyBrCzwrfHvrN@~mJVU71>#}Flsf&?_*^Ju`Ab@*FgTt19#Oygn{IastAhqQbP}Mps z9Sk*?4Cz(;h>79={(zLCyFnyo}*ClwGjN+?jFB`@iz2 z)IoYT;{!44FI-#3f{GjjngA6T8j;^(R;Gr+LQd6W7jcHV!k!`(@bG+s!4V_-ar+}{ zUYc{o^OV6Dn@-|@uKL<^-gq6)vt4I& z*=8rECD(#n3ODa|RzjJfvK!*$o3vJr!dITaJN-Q~(IoR(&gaaI8kBLjCD14eONEV? z+Eih;8et^I)Isv}MG&)q`%Go;J+3*Kh6hVJ@LXtp;;bwvy+D|U z>B7oe&R)w9@_Zx}K{mJs#Ct149)Z1lQ-3nM4TnrP(&>nbhc6^u=tFV4P4lcWKNR|z zXKqi?_A8B@&zcs5os_b@MBJl?ZvF5td?va3JV<{y;_*@yFcCR^zdT2!T*_PKaU_+! zDMNT)X=?05Zy?!wlL$D;nmrbhNKQdcU*5vz_2U|rIonsTMziS;@nzo{5aeS!5$q*; z1m*o8Q#F2R2+&@>!uQwSj;-lejewO+n?el&U}-U~my|)Tt~NBGMtG6N>uIBMr8^38P=R5hAQkKo6kp2d3amL5Fu0k zKZIBMrLBiCbm&N6R?==YddSyuQ4mSoCrGzcZlIe*@8mU{?#3GJDjACq zasA*wpc3fh7s9S=gcL@M4H~yIyUu5A2(>l0LncvrJ_tzZd@j|Vkk7@g30S)LpWS-a z=3lvXmu`;;#s^|iMwRhM@#^Kl(^mfP>r;oBzrqFACSx1gcXo+<%DWJ)R;0sxP~IMg zcS`C2ub4a^-~5jDiC^HX8}~vpQVM`?_9|tz=LOSRo&ApHb)4`>J#(=a#nM5QwZ)|bpDL;EA>US7&m_^ zbH89Eb_R3Q7RRBI8=oyNvM5V89Xb_Xp4%e;d-{2r{iy-cOnlNaKTrlX-v^OL3W^Pr7 zRPiD`GCI`{#HwpS&o4$=DAK!Yw6Ru_{x%N@icudNqF2Hl;63To!L4NcF<~h2fb&!4 zV4%75lJ7jn#@;~z1;3HhD<@WW^b)V0-YCvF?S@%Zc=^6xt3+kB;eFH60m7Ry&m1!g zPM9m%)H78}B|f{nZGm?6c4?Kko;V;wea8(BFucm~lI6$wRAT=oTBs$*{OZeNj|2Gh z85-fo^p)2!bl>#jV?jm4z>6r)=`NS;{mY?uuE1FdNdAi%4Z=c5Im+84X*0dPuM|-?ywxE5_t3 z^Z4`OpsxLqHV~S@zLJcud3;B{cZoVKxu0^57^5 zI;qARWNtZbB_=j$Gw^hQE8sAjV)@a)hvE7U9tdh%Va)K8ESXV(01dDzPHlwER;zrQ#DN;8*X{}LM|Q%`e9A2iZR-h7}P z@HeH>2-X+>1|=ceJ0>%@uKZJ-@Hc-#C?2Az1!Mm>I~#+cLOM0khV%%EH#C1dsxhvz z2-Jg}C3RmI4VPnezJonv_mof1s##U+RWi7*vCfX{%o2G*>i8~*q%_VyXp>@v>KR+C z1Tk3(GzX4Noo*JA+KR3mBYkb@0?@mUFMpfgmXrg1Lyz>4eDv|3vUByxY$w+cQ9HoH z#mlP<{V{$>PuVsQp(NM*7!-t(|;$KbnJFoDVCQY6F%-E zNM>;@^Q(zQDd=4|t@98_I4b>J*s!B7-F2pHQt7nV79u6wMuJ0woYW;m%T-Bg~?t;rwD_Wu6N|u8VG7gfuW?L!*@;kCZoW!dN8*Zk`81J4q0G{RfP3(BOAYkn9U)!8l-KSlpl$&F>-wmK7!dp02eucs&vXU)Lq!N zoC#KArKo28L7RjG`Jn#KIfrexjje6tJj82e%#4FdF30}FTRbw`wY~{XsczuR5Ew3w zoi%1sn!RAFR1gg7*B}u8zEH%G{g0^Hqpuf~h_rvyB>&&=}oQ zMY&_Ho-FR*nHr5_FR8=BQ5WL$p-3zj1t<0H5L#*t3eDP_mr}hL4?_U`if6?=v zWaF+Wur8)Gm~J9%n3oZPlu6x)E)?*j7AXT(p!wj*4KllMN2Y>4BfbUa2%{>9cij3E zSW~I*%pW$ZLfN5v9EH&VND9I4W%!B3LxK2G<@P|%Q}Hcnj&iX@DIN{g;(LA3vo2)s zlWqm^Z1JWq>-(w^4rX0V`4qWGLgSGM<#-t0sC3jz9?DO4J>cR;|JE+_NHuNq;U><0L5)mkXZy-b&bMVN=hn3Sq+mx;G{(o{g2k1ZXE(ijn(Rtr) zpif+F^Bs&|cqw(ea%g6+R4euV`)u}>0karG4Kfa8&3D5GWLhBEGt<{EA3iY^;HA|F zKzS{LHhNPvD*5*7hdbP7fk4<_leJanzkamnDvl&i5mB9n%o|rL#ipla1&9Ty9CxN7 zPa8$8eeX>?>T%|S9KA-25s|n@9C{Btj}6I~kwWk=WV)$~3fEx>-X!853q6gHq!=I; zvS;4=;Oe@6^`xYQt`Q!yk|SNja4S|N5EDA5AIXeRJwkU#fDQK^e$6%8B?`e~bi9FQ z-;?G$y=fKT^0~)?+w*uzC+H^`=w=vUwwpL9tbe`K{x{oS%N%H zH$Hzfz8g7VGAl&G_rnacSs?PWGti#vc`|Nh$`&{%i|czAbfkOOz_vEVZ*`9Bl?xbq}p)$y;ZF7Aqp>X+kN-T z$-al61Fnk4fl`TqODNDt+>DEKY+VS2)e0%=x@;s0LsV^Vw&^}&%%f-bn!I9Vp()3; zu(i238DT{$^-@5`_hN5ADBV1j^0|DAY;^vIAGWM`*9J~pnTfbI4gca}?F@E7l!Snv zmisX=e@D5WF)!L(%>^@*;lmZ(!=baH0AZPDo&i4SU05EIQuGV~y|!gxn#fj|esR@? z!%@vEgW4ApXtoFO*Y8BT1EEjq%tL8Iyz-P>zeM03mH+7y&3UbVVbjPWoKBE1wF#D+ z3%EmoGsWLL1d@boMfVQ6SJn6Ft?HE5VM_5Hi`9sy|3%Hm`VU$&^x~}n7ZaKkp2_+1 z$?$&xAZGM#PM)1B>BVyB!K4wvp|bSFDJyn00Z4T&8LwE2)sJ%NI#AkVVgcNooNp-` zX+PlV<9zo0bhGpz@JR$JA16J7Cae(I3(d-=zSJFfhN?^KK{euhIq0^aRg!}w>`3d+ z6d3=+7IzZq$L;+G?`u3reBe%JtRO|byaD1KntMoKKSzkN;#a%(z24~LBOM05m;FP* ziZ!%EfYm=o=w!eT9U$gY)^XTUpPX0dzYmDRAdrZ_DkD-*WJL0P z;qREmS0-oRuR-`BUwF>8nWXHSB9YzEP4zx%bAi2sIFGizgX?C1B)=C#jeTjmKfNY3 zmO(YC?20Pl?2VGY)AeVaM>v@X(?@Z+PZ%mE+gk|Ky=g$nVKeQFR`D()$_3Fz?^NIB{Q#gh2ap4 zYYseI2~Yjo;Z%W$KxsB{K$ofh?roi%X*6=91w;$#vSz z09+4=VOg&n&fMmysHS8@4kiBmJY`qI9P`$)6J&nOK3T~n` z&tS+<8<5Q)d|JP8sO3T4hYsC&oP$y1@+MbtK8%>;GP)Vr;%F5#oZW|Y-Sr>hQQdYp z9-VJXcFahZKQzksW`zEULR!N*f&Wl~;4|rr0?(^*qb?=^`gI>=W4#??qQi8C0p|p_ zrj`S<&(h1k+8=D5H&rZ>n*7RB738KrjL_Y<;-XD-=gaNP1t%n#h1v4`F*FO)1HId~ zm*`sN*7azA*?u1@v6|iyXZ)wFOaAmV=~+WRABe6YRD9fg(Z1Raa~N~MLb9TGp+Y&( zDd5qG?APAjf7k&W&T+xJ*AsX(cu{(or_BzbHNd+|enZjP-z8#cA&sH+5}D*?PIf&a z@97{nAn)t}Nkp7q0B&|OFf+O38&E36sGpV2y<<$FTrF0JANd-G-hG+Bkzh0RK+(*S zrl<(b!+vkp-)kK#&;DPyf7Pi;MQ7QRA_&9pPsh4Ck$&m?q=WYM4k$%XIS^PWW>C$8 zPw-P37Lyi92u^L-$?({5({F_#28Q*Y|KtEnPSJHHIk%Af#3V1dX&`1qfr;0&Bsm8O zIPjQ=KR#}Ub(I?j@jJ^CLn(jtta?1#(CxW}S{+bVP*aAJ(+Hiq-cqsul{2zrtT7>W zcWmeWN~4m(W{Vtd?2kN|di`~~eXpV*lJzp^`?iovKa;d6Oc;Z0(ITj@-Qu+Uw!WId zt_fkHRRfPb%qXn6aR3m?v+$a!;V2JnP%EAe^GH)Q9g}j23-f;x)n_S5I7T;cK2~A> z4=dn1?jL|p zX{bT~tckc3Ym?kP&-#nWC{g0$(kotj`~#DXr+P}&@J=$x?VrTx>Ns z6Eniq3I4O}#)YvkQdfUl4rEdTC8})-DN7s>xlDwv1`slvToQ8lh>7P&v?s zp!#=*ZA{2+56lPc*Srk|BBmmpo*b%;0NJ=FU3Ans(orqN-|^RZVx+WCdU)*Gb|Z^TSvZk z?Oig>;5_^;GcY`4eO*Y+g|b&Qx$Ug?$1vP_AAiF}z8C-{Lu?VF5C^p`N&kKbFLEJTxxEs)H`w`4iF#A6 z?N>A=W@4wJj2tu!8?3k}BrX`!@D^tcP-qV^9IlFCfT+`z4Vk1SS!Fj$Dk6}0I#6;~ zW$8Tm52};NIMp%FDnHvl0JA|&VYy{;4}v8p;+tzb4+59f;Wlq!Do-*tX}^_itDZVq zmo`4|tx|SwycNj49qP1WS^cS7ukm}uc7HC=2^inKh&}j3{ zkW2neIaKh@;bSk|q+cM1Q9;dJsZkWQ-B9Ri9Fbicm_r~gIB`$;+qEyd5%Eb{oIjZi-I)K0}eFxXR>xzh12->9j*+aT`^qb0`#XlT_VMd^FJ z3i} zH>n^a$K7W05l8$FE>4t>0K`GY%-J>GZ(mHz$mqAkyweK1aUHQ zySYLnBr$$O@}a(6Xmw};J!%LqAFgLnCY5_j-Cc=ub}Y50q5}l%JsA&F;%B2Pum zXVP=tY~X^^6BDjX-IN^?V(zDZyvx`-93$hY)I+NVW1f(;0fo`krE84e;VhI)j_S5uIWqm_9;v+)au9u?m{PCd@fq)^s_igX zUG|K?|Crdk4F8$dx=Se$VDFx(XW-YSNvnpdYASgv46A9;v6OyX+p=GlY&L%-Z9TNh!Yu-LIaLLhRrO1Kc%-Aj~z4qc@ zgDx68Mn(cXRFu;Z%o?EG^Jk{>dB-G?@H9p*k~g*1)Fb!^rGJb3`uiDY6Bcvu~d+*IL^>m)wQAwTlem5O=~q4}($Q*0!8aOV%S30tlpCG{$}C{SFcl zaOURVaO*-39gh|74pCxuXsO(f(^TW$rZvvDD=Sfx^4fg;{|pyDzh4^u^+L|DVYj^p z+vPp6g}-W$)&j*+Odj)uvKZcI0IGa?R)cWc$`9w>aZN4q+kyn-=$V@AB8lK%&}~&< zb_ZY=dRY1m@JBX}1DFmGES*C;HWZ5~WMUt--&l-mdDgKB1_E1-&xrm&YBTN`-YtKn zSWEXny6?Q38o@C5vy{|Mo3vq%?0uziASPy|yza~2OqxBLNWKnC$_}Kj7pL?nti+(lx6jI*kAijVHkudeXJBJ~klVK_sVc^;)7g?cpW$u~ z^>#J!=h>vfLc!lU;Tb%M#FE6?AYf#YSlN{XQ8(Gm+CN z>rY=kO~1kr$W{XxgP%trN^I#Oujk>QcbcXN{($;!T%wV(s4D(}3?X&CbM;k@$c%Nt zBS`%{bcQi<+ErinLxNnzEDTVtua~Aat82nZ`v8d^L*+EnU$Tx5;Vs_+I@Gl3(OdSM zAEKN&LHu<14%|y4m?#X-lSu960%Bq zuMhM_>Mw;3E{J=Tepmgs@PpP8%N8u!<=94y*QX_RW3(>VPKpgtWMsH;XauQ$AFioq z#;?V>NlcduwEtTnhFi*0T@MmaNf#EY(aMt|!vv7E7S!vMrhP-i$@mzHSX18CF+!Cw z1X+}P!`4@--~yX?wSI!l3gUqPzkxD^%y)XoY$n>-UiPSSJ=wnn$81?2(6L*t2Ceuo zQq~71<=Ev;Kdd+5mQ%>RE-JFkLcQhMwu$}O#YK7XO9_fI0TPOgNoZCwv~IC>#(Gzv z;S}oeLHcFHufJ$N{8PDbs9f8EC@P=8f@7#^?Ww^(lH4SsZt4V}OyPF1Y*x{Y833!7 zj7YyRd75AH7P{1k%*%q7Mw6n+LxedzwppM$U!o$BOqP$fY-lh0H|$oAGYu z?Pz44Rm9EWa=5UutL5C(@6`rQIUiRt{F1XU5e7)J{@ycc^0mUbn&VG#(Ui7zR0;Fv z9+D4hVw3UQRa&}w$gmUAAoEJ{ISmwNY4A}ew(t+1_tUb?_8U&W_iX?UZzTgj&1(J0 zNka5lto+v&GBe<~C^>RpV@J@ZSqw1AqqjVf)j4&VX<@`3jL0c*TT8WN7E+(oIw_rp zoRIlmwN++W6r=P&hVyGWlo@v0OQaxS8WWyfb4ybPR{yc9b(>A#309_43^A7(ke&S7 zopQ>7!oi438WTzY#!MjXj{wjaK$i0I^X*xYjAJ7oX^xDuaH zfP-;j7z^gkC|<_CYx*&igdi1OnvDBASRI#55`0Uv&nWnjUb}#Lw77}Xy-z5O4jR4v@M-4d)H{XD;IHNQl zl^KiB;ybGSPt*Ot9-?yP9_DjOZ>;g&3eO;ruKJ#3aOIXZH*#4S9!(ARUKSzFlT9;Ihk@E%vhvfP{Y-ttJXo z3nCBUiJl!U{T=lghM;!3TVe2sv4o`ERPPV19ZTak{TH|{^4)T|8a0hlnS(ye9or%y3d9gB>yQ?G! zvm@GoVn9vYf&Dwq>z?0@kR>vLhV)&&YtS+*-{?6Vely!ihqR4}NrsAQf}loYM58#< zuZ=qfGa8?-b11i@(+2jP`Y(1{-$(JaFSpR%)Z%-X=LsCuv%KKF5t7|&a86TUhCX}K z<$CpXbz(N-diH^T`Ur4qTtTh7J#DHdNmwR%3 z1RbYD{BA0IVK;z##haAN#V{wZ;0{6$$X1R)lRH!}jLHL{ibp`7K3#LvL1d+L|HVX^{tk>+^saW&{Vy=Q2X6&EXwyvQ zbwB2$VDs0lg6rQTz~t==?I_YrRq3p5%xiY1o)csSpg(loxU7J0u68ojkA@y|$+_A>F`awRb6Usi6!V2=^MK^*8pGd+6M8^CB5-E3n!m%OdLZK{G@pYesZ%2g5HT(g zFio?~V)M^jUQAppbXqkEYp;V|6hEf}DZs6L-`Gg4LvmeP^LGqilmJOUw!eu@VZ`U@pSe`ryIMq4z)8yNuUsTrjbQrf9SH|HliFe$#DoG@2t%t^ zaQ$^*i*=Nt&b4pkj8w);b1K-~2DY}thp>>(v$y~?Hoo6;;Q`#W!5Z1G5~ZW+&0T9;G7x73hV z12vHU&tEcFxT;ka+A%-8Xs_6c4^(1_2=BWH(?_jV?D_lr!(n05?p$Xww0A1+l>;)1BPZ1ht_rkYuLYEVmm`~ReW6k=?t=Yol zC7tv~;pcZJt)8@)#vy_&cyYp|0IEbl^^8RbO>N3mymVzKOY(*nzcd0nx8Agu&4c?1 z+*^7YyQ<=z=yJt3_v~TZ{?8OG;hb9Fd;L^sDt$n2>iWJq!Z%Z1Si%Pwsz5@ zZJ92CCm=|F9DHLP-Pm|ES^kH)*E>Es0UBp*9RftIL5psFfko>4eb3eecpu^F(=Xa( z27mqp(hQ|cO>jL>Yi<@lAoz6fV@8AuE^Rm^jVA>Nt*rD))1pBk0T(`%P{LBfkPtkR zY!w`#-()Z|)u44kThcmwuUF7;U;+NVY?_2%&%V+$s@+p6woRyBGLnle4t_ZJ43wqE zmM+4e=FNVNp#{1QhtyPzOp%^q&SvT_^gQ#SAqpoMm0M>=tz;JQ@Y@i9(ds_snOV=d zAuhn99nsBq-uxV@cOJd|mFRtF>gG!#9i}qsgq` zyE@I7(c)h<7EV_{_JhS`vdAOUJ%}5U*8z(g(Z?q6P{ofMTv? zArN~=L*{l~=S8=dH!Tn>_Uq>*qLv+bY}uuSL75y47&7S{xu&z+4uoJ6 z9{cgX6pHUlG`qm&B1S;tO>$?x)hk-Hnp3$vi+uX+4dtT5+px3Wdi_CY z5_l8?O|Kuy-1a}Tm4+tZoA4~#Urs^()CEp1C82=n!>Es2v|?pp0ej_7~Y4{DxN?Z<&5tbql0vDPugv*dkCv}M;Emf?J zvcU{E8qw1Ay1FgY+iU=cC0UN(sZLh%vLg%?RBAT3VEhq=F28rA7kV3k>=H(W^U^{G zo}8gK^J6y!n;oLcy;KGlST$7(f?srmLt9tNh7Jk%9d0Map%N>HP#yF0B(V7<4{TK2 zs7=%Dre4zFV460>iLIzl6^oCDRd$2=7@?Vf#X01ED|{#U!)*kL>V?YvkXOw{>^}`` z;^v>2QvwVk5a|$E^~d2#fk(2_6+`iY(~oLVCjwOu%I;oUnc98bVkNal%jQ`Q7E8Mv z(Kr^y2_F!&dc#&Ezn^)3C`h)m|K8}B89-l5Z$EZZiyu=ZwQ|TG%(Qql zXi})FeZ|N{p#^H@*5dxv&?sQw^K@`tHF)R;WtqLbprEDp1p~}OAc)n}^3DEkDpFdSpVH4UgEASj zwLE!uNxleCM}IRtFIyU)q{yX_{Cie=Y!}988PnGhZo&*Q$M#w$J=SA9VK1wQ5kS=- zh)?T(Rm)BO$4jL@pP=mi3I%sE>dm%=@tS+cFSfmkSO@v~K|os}U^fdV$w%j-$c;g- zU1nOs_}(0P_{4GR7qXGJJz8lyoOyf3dtur%S)@16_HLftqXfwOA|kZHnq`X^^`2ao z-3bx~0=CVQ@;H@$W+gp!m+v1ze!zLK@(+~|QzaHALsPeL-I!ZR?NHS{vE>bJcTLr4iKa?!vUH7B9})!WKlIcnP1RO;xCPUA zq1ElkVE7rOl&#qyD_Na>avac!l+OM>o#2@FFY-6`#$1Qvvy=aLoODy?A9qK2z~b2!be@!ISO#;>RIT=vNM&Z1zxHVt)em3mwwMw1 zudtsv=4F>RvFstgj8AJA5Elim(lplq_k#+RLUal_0JYi*dp6D^-#BI1B?7O8s_VW*@~x+rv_nC(DcmtuSBN)qSOc;|AoN6HvVBr zpPxN4t| zFzVWJy&AXBeQ@k}t1dbPlQO47sI!GgQ*^c6#8^nPGUTWK5NMTB>7vw5Q>10C0I9fB z=OUV~=5um2Y|7H&s%*p1*kL;!aYtE;L*kc&IIsfhZ!Ip<$Ht!RSUBp@QczipdJ|SR zcQ)_*^C(P5q&qocUkR6E@+WtAqX4n5OBz)aJ^0*|mMEzbw^rpoYl&KN#!VZqyy#*Sq>c>onnS>ZTo?0Z z7P1G)9nTTz=!+6O^e%0;1(ktLZTdWrP@1udc6ph#84=wX~ut;dGjF<2Z_j+zSZW!She8XQFgDU)fKSphvbP9Jmvn7y`kRivX>UpmNUr zSA3rI5j#lKSfQ@(jGxnJL$vQmt5oqo1W?iU{XpwyQ%$)lXyarp9g%JbPY;L^OUguI zIu%eDbTwFrwr4C>xc4&;;+2jiW#f5%G{dY&WpL=r>h7X+z$MqgBJCt-%o9oZQ^%xBd@JdJN&S zI0OvNMK(E1%XQKvVP9EhIsX4?GYiD^J%8rOyO4?tf8(|%`+Br4l=G6sp&S{ji-nnA zbe{bmZG@#X6Lxg5S83_FCWw?%Un8z3nGndyXsZ=&II(7WM;H2ZRdWk<7y0 z0TJ5EizFFBB0eVsa!gE35evH~=Ihbox$>K=zk6}4Ms8P3iThB-x^X!^*z&L78>0dbK6tcTH| zNtwfDdHO(*1?mYEUujb6LsZ+?N$`8CN)Z{S{)}ak%lo_D&{7VeYrnog3OVGVYrgpWxMy)CKR`2i z{>H8iLgte{Hfjzf15{2#k5qSMIlHqNJ#m{EK${n4C}+dHkZ%*a4gNvhXS36@ zJ?|6Dst{T4j1}YZ-Ni>p1>E#bl>r3u?`zuQUC37L82gO{B8Kcs6?_#u12b%fi@!0- zF1)rS$C3EGNae|TE@e{r))jZW_fRZ4ChCy`rC&`j0gj=T9U%4yg0UgnMENEPM7D#1 z*5>9)DC3o66jinZQuX88fd&o(eer4eDG6;+zGb<4z0sPeEkZrbGNJg*S_*ff{|<`V zl}9F9rrpxkXt`V_U5Tl#u!rEol%~T6K$8Wf#kQ;@ic(tGSQBiH74GibM{f*wC1!ag z*x+EDYtvA8Hq-(D8V=H3y7CUaP2IMBV0^1oiZbPcP)eOSC{7UbT#Mapq&LNCwq9Ij zs%Gy7@MvG^2TQx=G3P@J(x}o0$I;s3=vkt}@ggD4H^Y?W$NW~NO6HkKX=BPoN9i*+ z6!IueAVU@5< zt3HxgfgCyvotk!lO?N@@%@ieMejfEFLBTmw4;fzB3g*5W^oU*+vfbzz6+cfja&lz`9Cz<4i1!<{XU`1!(ySUm8CtsP6ku!vk1q7sb|FB zS*hk7N&Lexon3>g!#tnfwgq_AkbnBlnM%x(QxLuMkfh!TRoZii@q+`oj94TOsX=VWX&KK`v~#EcYx|JC zLF20gH<$p8k8GU^tHF+o-|OhIV9wsLp|R+nXb${bcm>o|nQF1ONQ*y~7#2GL#c0>2 z<-@FrzM4Tr>n;@df{GFJ#w$y9fx6a*5tOtcQ=j!Y?6n|!-0OKCn>KN|BF5LpS zz{|#5u?C*8Fn6)QxNuHe-3~FofjZ%s>`Q2rvGW z)Q-NjGAKjr=+{UEsgOlq7UG-;KJ(Qt;15uLch~1Wq{!PD7NLEs17V5mMgY zE~*OJZpr3FLu*bXu#IJ^5vx7Ru@R@Z8b8Litmj#6ZL1oT8Swo1MK6B?KY$fZm$68t zgQmh;QQ2oLy`Keh0bLvQcQnBFVD`XG3mnOyZf;qD;cX9bx8oOpM1Fs2;$+)y|pCm*oE#OgSZ6@_D{uQP%(eib4@SERLm)gZ$)$d?G-i2>$ ztc`^xKKRqM8WPOj$I5?*-1r^Zhc}!JThCm=7-LkeDLf`$t9$9f+(?S+V$jj!13!K? z&v$=^ZN{US97?ILwi*dDK>ftB%g*cV5h*FW_a8Ro|Hs?jxgzMN{XVd)N<8KYz-mP* z+8TnpB5GzSJ(sGbXpK}E5q4pKk2&B`U9uVT3my3YVCdjg6B-9SV(o%2G{VCk6o7Q4 zn-27iroM?BItSC&L{Ax5AGPtXYBY2n2bu<-r+RQM+7o$}}a_~Ga4F*Pe3&NBgfGLE__m}eL=Suly zVYYPRQae*i!PvKi*+&87c32vjJUN^)fqhvd+3LF7Od>-W?j{G2Kr3WNF zq`t8G>_1q31g>dxE>vy{%H{nm{U29AqETYfDgU&swF*y^_(Qi*LPXhCQKUJx?lie- zCOUISYnV#C2n?e-Wc`_ENhv_n=kF=8-ez_8HgKr!Gai_%Wocd0VxbxYubwu2 z_f{b3<2fQC*a-jZY;c}GVF=Z|*z=eg(}V5L3R{SQIHJYaIkDW5RFyC4Qp8PBm;~#u}>3_CF@{n?!&# zei5qy(ypBVe<=hPK-c4DjAu@Lha5|d$t0hLrBEnuj;Zu6wzrd;o ziFI3$YYe8w>!#$xMkD?hKPp#`!hyI{s7&op=MbQtrZ^V^4!w-cwSHhCszfFc#@I(gt zPTVbfd3xVs%*=h@bgy?0()1d-lfe5vv|13(na8Hw&H7gWy$D(!mFS%{D0gZ=4_4bB z6UfLp+UvVV0ZAXq=(9k*0fHF!8z9Z)ial%5b^>5C3P}f3iDY=8H6mXlW{P=qq2O*lX)3*i|7OsjpV8{U@J2_PV8SxAIp; zaMxzIhGZUu52H)wVQEB-Ja&i~UnqZohML$_K#O^4=(ZLK=mE8e(Rtf zaWz6@O3HS3B4};BBA$(oiV82*xz7*DV_Bli-;^coiQMu44E$Y;(+ixrjqQq5o3&mXY z!5BxI1!>W`ok+y`TFa=(_e}N|okhMi&TSb81Zmy6w%sLYC5f_i2w7o&;#l-S0X7$D9EQ%eyBF zRUSVuUhDX~Zvf*V3%`u}G)sgq0g9ayic&?6!G1SGmf>`wf|ZtsRb!p@m93Cc>3~cr zq+bZVKf}|~)fIPED(@e8o9At_vA_C8al+tzl@eAA721r`ErkgQm`^qSoUgG0**x(p ze#jb9eUh*u%ToyYN|&$Ui*=|T-Oj_|L6E&#;Hz>adHb!2JqG-|(W{|OYzK?W?3Zg3 zjZXPUfzrYJaakycm?hZH&q>|!*fsa^WRFemKD%*U3kuf6sg^VTXA+7O;S$#)>;!NB zhCMe&I{&X(Guv6mb3$**o!johXC~vHyYNr6n;nlLE4Wp6OrL4c25i@16(l}Muq8hd zs}=(1aJPo)t>RHJ0RQ3&b;Gu7EpB`p5}?Ad7Cwa9(V`useqhZW?mxPZVT^Nf7x&bcCmfFX; z*e;$-b@XbX8fG11Gr``PMi8FCJ?GDeo4!AA$iP!r z4>K-jN;}=(wqR-DhlK~+4nc*%leoB=d)HG}&evM?o>oV#1$CN}R+)@(16on*6J_dzq0(o0WgTJ(nKb#E!>D-j5s2R>RVB!q}7K?LTwJ2NQ zVg;SNR@Im+Jm0NNi36n}WBRuDp3Lca)#7>OJ-s@KFQ6o%PxHj>f|ZXC^jn$eY^^`P zVqk5PC2q|WFd!7M2^g~=6#dpoc}Y6p0J;!ssW#C7dn)RnRFa)ROc~N(*^m6A1R?!e zDX#?vi8jN-ysM6e4Ll>*$TQyGZUmhnw<*V-aCe@J0!LbGtKE{4uAZ(7_ZiV8>wJUd zh3Cqhr^*XqB*DbHD8Xz!9g?m191g>_Z_Truv|>X}uia=HjGgWV;m_@1Ch-)eOyN9o zXn*~byJaf^z^h0?hzN=SH@wrgmPfWU4MBrW4?`hHHM)G+f=`}ABPSgm!V6_ow-{{c z*(!0BZ~$u{eZBW+goyKk?Zhr!2**oeHsjwDm~B>EY>vZblogrn=V;`i(uFr;E7ds zw

Z%`^RI#+=KCL{3L+UZ+jW7BhyE!k>?WtA4F)o8f05$S-eP=<)~I)Rr`alZm<% zO1;v8Nv42P=<3nt7%He20hX!)Y>}&pLN+XFB8_ zCMN~)!*12G4k&`n&(6Mwui5j^&>f6VN(ama@_}=h^UsmniIhlH>0N8MA?K{e3nNhJ zxh&v42CSiTWnut?yI+xrN(s<19%kDA3Z^UjtwBTWZ6rpmHI(3Gy!e7Mg@)_~oxHm> z42wyb+9QXm9n6+lALNXb@SB_Joi%oHVb|wg1r~#03GjM1_KB%M1IFv4rl;*gWM6~J zvw4`I1VA6oA|_5Gy&zIuQ7D`L2EDY!N};|uIUtc=6@yXEkqOD^61}4YowKvA`b`O} zW5D5O#A|;%Q{hqv=CUKeyTD^EUy>S*l&3un<^P^fFX1B^^}q;xTa@}paKuGb@~*nn zarW$~jSx|H53)oBg326M;C$H&BSJ(n)+#)1${BZ)u_Tmc;m|aFW&q}184oXcEW%6T zjkFU!$&r-{jcl zL17dikXYeR4#)J~N?B)F0;rN;AEx3RQwMuM-h4>XVAVk^k@tqryLodp8*&XTtA6;E z$G36V9F~&Rf-f`V!o3^LlUaTL#IB=t@chUL5&SoR8f&o@xTb9HCLW^m&BhYEB&HOc z|K5wksBXpyGWOmqEE+nWJ=wB>Rnw_F8%LjjCdG}u&!Z)>`x|R&o&0+AdR4#PZUCgK z9<~kU^vu#4_B4pzC-kc9wpBAoXDY$jN2pT5Q_I)kQyal|$y^9^kB%8dpqz57`zD?@ zE*daoDSXJJHU+Ag1R8|$fN);G6imYy3FV4WVO@ILq8hg7(QcA(?LH_v>l>m0I6rLU zKbI9-S*)IOi@@K8_Uz;xc4V~)AyuMu9dK2Puas!Ysr3GeUVUHzkSlBx37_EhP2kES zdCL1({wh+m9#pn2^o6^N;@k^@NIA^XHo(ay(H4&uAnC0w~eYo6KRv|4eph;DsCm?pJ2wvGY$b;7i(PNW&A$ z7Uxn&+D^!^dP2y(H$H!Jw+UlOWCr8DL!|Jh!m)q=W^P~6Y7@64K!&{xPcM%y+~Ch; znypgr3ia!nN`zBjUGKmXC!cc;&*nG!3`-LGHuo_rv^ENXD@>JAv?@Wi0(f|rp>CD9 z>dwV=ZxLv)5$T2tvnbj`weT=u996P=PA5e^w94{+YX1%g$IER$MAQ(!l-4jl)C&}@ z=x9+y$L|JtUk~v9e1ThtgzE8eK8cj9XHXjd)hZm-O`&lAUIfK**v;GF5*27tRA+U)*DKi%g^Aj|nq-^eUC6Jbuc8KQD*r zQ=HGIPs$|qT>TT}yFYThjEd7kfEIRGfRmi>pj$ns;jDOh*qOU*0V*Ku2-JP4XaUAI!9{5Xs&nuknG$3o)$`g;?8>&WMf3+qRo_SK<|3gGN!&8z4y>Nt26^*gyrs9f3YJw- zpVZ)ul*P>%E-j6>Hy@+L#e2X0y(^cwQF!Y$THgm574vi1LyGDw^$EghR(-WO=-V)f zng2vc59r?IixR=u(dRPGeiDsvue{CG2D4csHNR)Cu{)8%m{oKKS~^Gl$~LR>FWGu3 z6)U0UXm(yb>m8`xRpJ9=UjvXqG|ZH=juUpArboHMogK#FvH7-%Tw6#GqnvZ)n8%k1 zuNrnZxMWQIgfQ4S2}v+PwP@X}(}8bZ*G9&((ii+#(Q7lTXKsywfb^h1z=Y^Kqvpmk zZhHmX*A2+PdG?75U-Cp2M2LIo%h&K}f!_XSl2KxlF$dpB>ifc{ihSROlM7VF*X451 zi;om?141yS(^KDh)`15jghViZwTqB0df$(q?^NAyF}$UzFKsBFkw z=hTk71@eysEQPX-@mvUp_X13XUVats#P;o%O97l7@^8T<^Y+oi>w>4LIS7-PP zXG~IPyz150#)OO4xK3mOkrMQ6yJ<4!NDr^&3(i)-ufWI2C`$Z*YvMqy22GF1&u^AY zNgfLP0!FAyJ=rdc<6Ox=l+Z1WGvckItypVksrYSX+O}w`WWw|%4}+tG{$XGFY8^ez zB%WW`88bW{Tz1>VnXUK>1<|izt2V!yww_y6&P`Eq;s%$7WD?%XNd$ z8jJ+Rk2Y4yHhUdFX;^xzis~*XiGJ*}&!^_p;drY2#v5I}jdycF#ou)Ai+{sEz8553d8Ssi?X?ecP;sb0eK%-$8zG-hxd z;g@l^D|qxmC#{%%%+|Do2=WIqS?(XQvBkT#v^A7@%H`WcD!})Nr)qErHR$t7!>(gm zu}fCgBRfl@vn3)90r#qx(b&c6e?hfD#muf8TmAwqmrJi=2Mf+Za|#wzdWO9MF(mF*<<}WEL2_1dk0p zS4C2i!~w?9;0b|de*6-?R+m=vz*9y2s|#zSt!x#MVRbJ3Ok^i*9H2r64is^tVP9tU zLFc~uLK43=6-Fb0P_aE8PXlM$Y=I!n>-;*NjNNvZT5^d%^;%(}%H`d{<7is6X|-g@ zF&^p*J{F3*j=M8OT!ks#IXO`;-yH_aDdxf_fOlNh@qkbs293dJ%-Ldr`~<3F{Z;a%xzFavnO?*O#R?0D_GX3d@$k%G%jn}Crd z&WbwS5VF*80Y}gO#s_t=8Ke7FHqPPJv@){<8N&-a#$kLY12)VdBqmoozrP;g87TM3 z1B?cNDCO?j*0qDST!Sb|f)D*-9!Jy!*h38;u0v}eefu@*I7c@-{!PC~BPF`)$!xXV zXGF+u+)ZraCIZk?!{4oz`j|K~0Q=R3MD!!^WNEVfv6}YSz%EC?)G#+87osS^MUiDt zd|#}9*|XtB7jfhh3W+{y=dcwPg#<~MX$Q0K*8W$T)~#O8%^fWI3;5Vm$H!9xi}o)L zmu%#vi-?vOl>tBqi&_aVDA)=DB#g1S+!#PZ)!TPnOdHg)vtB`htP3g?^Iharx_`UB zJvpPsB{F+KwrWVoG#ZNGhL8ADo1lJoNeCwjDkqY2FAAZGD z!^u#T?s-1G$9KHq*sRh=(z9EIaYd_3AKXrO>GjAv)@~OV?xndmsF@KSyfb{M;&}z2 zjb~UT*T@LyPG8M>lzEzht~(C?7+(G-*S36fU1^l07lfiEItzVz_b*H}13OfUxYLs( z6TF9z`@`+sK)cZg3#B;{#3)H6r*$IoN0tw@QTmmBI6kUA3oIGLGP7wSgx8uLQG_IkdTm<&bpvc{`a9`5*OCd8i#8L z`Ue5A5yMX(PC}ep49r9L#|pOhn52%XdhQ0;l9LP`r{l1Q#03|nJTd*dRN8cOVJAix z9@poYmKmQ}jT0E6*aiN>wyg_ptm8f}*48gKUJ$om|J_Wj6RcO^n4THJkH(g#w;lTo zDvF!DydIvDzg@#p_I?%wfo2Kh#Q*B~Vj&GiKh!6t`!Rn2rib^kXt*|l!}W;O2aS9| zfbGXIqOZW^uHC7eYkRa;Boy4(y1O#>UHJ8;dNt)@sHiPoU8kxz_0w zbFP7)4sv9INW4FIqig@FAl~Q_QX(B^Td!O$Pm4o~D`B}oW^hUtC%>3ar-$EW@pZ1Y zsNo=xh%wjxD`Ir0ixjdlp95x4iMhk0?#$Vf`#DsVfV11szepTuv=%f1C8Ay|=6Kb-8+9HzALve=l ztozK;kkNZ;9unl0_VU(P+LJ%LlEp((umMQ)!Q! zz`0D>!>apgG<%*k`3OiRWV*(QHp_VgvoRSnUlYP)Oe(TGgQ=ZGTHa(QGs$=z*1Rkb za}yZ{OSGgvr|v1`KvOmP6*R#QGkzz6=Ck;>|9>!t*@ZIDeob;_(&NjM1nmFC^4GwI zr?YSZ?^;9ihCgI2I`Um8!K@2*BIN)m1q#v?9VZ>b1H882Z8>$G-CMX)q#*u*atJZ< zs4FOEOZs76gdNPAbhrvV3aZYk5d;17dB_S@*OJCAdQGn02>BarrtbO7O#V52i-Emp zJP9SvfW>gT2$%|N_-%YsT-woGDB0F=NuIScA)k*@n+ zKaEsN&GS_mcKF8;S-6{arYxXTUpusk{qsmeF;Bt;5A-u8Rc(xqHwSs`f7s6npog#P zathzs{kV#@d}M-S+C12JpV|{xLfW#aLzjzeA-O)Yed*BhNE+ao_r>BLt;QY29+V~! zp*+DH-YlCVc_U88su4!=D|M+TKB4V$hZVPdKERyI6~7{LGM0=r&4BTOolXIG5e1`j zDPxh%?rkr!sOr(K61JL~0cP}|vD(?b=*{gbHx?m6++SN51adKK^7>Nm{&E;m@L5mM zx~gqDH1t&B(B??2c8MJ&86^KZ?tJdQmpmSj`kcNAb9`yGJKKg3$J_fr*-MDNo*Uf6 zJ~+26pu4#`uf;F=_n5)Ms&+QZAs8zp0({M~tiBzJ#3dB|GNRLdPG>9v zWKfh4n5^|4z+!Ihr!&riAjCg}+8$o7#wht|TiRcm%{?F?en@ODGeJW&LPb$_nB6<8 zn!oFir%5nsgeLE1n)xKw(zp1S6%?q_a1x)_<5#y^bRI;43abRZXL#QeQW1ea5JaDycGBW>W+>seayPq=oh8_fbnyH;xd4>;77^W<>{skgRL%a4igB+9K?%Pit9V z$=P4Oj~Qc9b-nF9P}0EYEG0+OJK1juCYKk>+170UoQK7O7~QZUZtPQn*R2Lk#11Xp z+D=dLimTH6CK}Gf4jbpj#~RDc#l(3LqZQIzhpJGuX83nF+8Gw1$2n+%P4WJk(AK$K zp7z+vXR0~LYbTGKe7E=16h)tXel(K0A5CNSf3`0uawN;)m9>5YQtx(C;|*0PNO|1 z>@Y&xqEPnryO0Kh73DRLQYMGawj zkFi=4G8{^`RoFa9O1RK5XxF!G1l0Er|5oC%@dAth37GW)EidXl8d3wsu=P6H!!WsRTlHV5X4;Z9hpxxN7u{ zv~4X@Dr&gIU9^06cucZ8I#zD7Vw^tZlsFEQvIv3_x15OlIfzEx46u;27)?9GP!9)C zl975HG<8BRh>$@Dy}05$mo|dMPg5mHK%?T2)1SZ)$eQ4dsI~OK&(cOZECitt4;_Rbm&|J4uk_qV;1j113-%uDR}ZhoS^e zNbQ{O?Z{VcW0Fsn*_H%g;MKldZDo4I)l8^cJ3Fx)Xe~oG6zrrNl@bxw-np-Yw%yMd z7|C;m8$!4;*$!zIUtBh3IA&^wAeqxA%bwSZ$}@K~7x0kc{v ztZ@M)!3L!5%!(~5>!x+wRjRBbNjzHQXWzA2bx3rUcgTF_tJDO6;hP2fe#N4`UDywR z2jYa)StHDa)-9cwK@0=WVl)49muk6QVD(-!)@U(ChB+!c5mG%AIZvWjN7|d$oFNP} zVb|qy>3QK{g><1b8jZaida&i2GGK;`P8}2x4SxM%!I}sogc>32t{vlC4O3Bbu(`=$ zU5dyV9&`Dkq|UTsYMQl*Wvnw@t3ZG$Z+p_A?*-nKA@@2PM4kR=34cEVQ*rz%55{sQ zFBr~o6DrfmaYNfD@JO~JaRi?cXXckHM8?mP2aU{fG1;1~f@`qddjeUe$)kdRy|1TLaPMr(m-b-E~o%!0l%e zd>^y9fC*%IurlW-AD43pS1uaSbORe$ftc?)+)@e~1}+~Dn{*j)x3OPzLp>X!WsbWs zRmI$whcCr;E~=_Mn4+Bhol!B)a2nUi{y2w|jL+xT^;@=GXWiV@J~$Y1lpK0)_+p9_ zg`94pWglxlo6PT>$$Wlp+TU@Y2swvXx_n-EB}ok|qnXhJPas_pw|A zN0yxm8$W2Ri54Z8L#XwMs_2jGp})9v(%XYgR|!x!*=Ft z2V5#7!{?A~mixV40R<{Q651TlG%czZIIxMLM9 z?@n@bJN6$PZzPQdl9B<5TYWz#Ex8whNWJaR*DJU(H`K$Ykx6dy#}a`WAxGj>lcq*NsNpwtjm3BZ;MV zN6zJ8RG0M-S}g8VU`M?hV%K0{#rPRBG&}e62X>x)0T`&~;azGg$zQ?I+mBKy+~_v{ zV6xV|4P{EHXn4n>v_eMJC&f2bO2gA>COyxA@b@EL9Ih4+>vMc32TC;X=Q$b@yBaer z5}+3dJHtMZ=N;__i_rZzuyE2gnCla0b-5SL_tVVNV}+WiFcpv7oSkCWD=hAWYJUwO8`zNpx|d^*+M(9ncPJW|J1b~_2d7M=t8Xg~d&MZKW0+=XBMX|%>Xk}+$bW#X3(&0*r_3f6`LQ!H% zZ$G6e=t4Cbf|Vd(3CINuw?w6IwH214s~I&(UqN0p*qCcJn@6%n=JY+ft<1*A5t1|M zUxX7V_<+|R`skx_l9itVIibiU%&@#5U9IE#n%9#J=mBmsSA7kls! zS4>zV?$983+Tn-`O@M`f5qQ9+>C0HnCZ3#(=9wyeZ2U>6puSp4oLZby+azjzV^DK9oGe;;CzX zAv5r{Kk!iX?E*^5!Ln@YKOAIb>>!%L^k<>@&PS4{0fkV?0)gYcKMIjBf$th>e9~|E z%u429O}POVOnB0PwtYt99hp0$5UYzvSwQmh9#|tIL9`HlbUHHqjKuVsL10;vMN?#F zgFU)}axG%6s*3B{Ug`OwlJvwvpOMwYmX@8WiEBoeDA$@NIz$y}3E-|4W$6>{cKQxW&eESw#qsS#dEm1%Oak*<4Or zKJsoy7IlqSmo8=^Qt0+TS;Fte{9MweudVBQChJ{p{gQ4T&4WzY)*fXdNiH_th_-HT zx6qOQI6*aqvmeX$9EQn&(kaHocl6HM=5m3PCNs)92S=s>MAiiG)pI^VD+C~uwLjN| z7~$mLm+^H3_TqFAGVl0W->YtBprpd2ID|3}CA-*Lw}AeQ;@TXv`i)=P_u1ncI2I(@wB5S#eh z^y{o~A*%KpD=H?}?i>h)`e9?@KIpR&4-v(j1hqWKUskW2V$F32+-^DaNld^sx|tUh zLYYUZ{}ySXhQs+w6o?KOJQ)+4Ql7c1V)#&Nrcd{>`PkLS}p^M2=wHkgIHdS0i3ZAT>oD+K%{b3;aq7p zQ`wQ{g*=8UDM~pV>gmUrOmuY$rFL9W&=R6C^-GwZ{_e#AgbI80(&SE(?%)mOKD*OI z9M)z4_r5XLZzw(}vL&@1rv@Ty3GRssvkV-`*XWF7yHw(-#c&ngGtUQ@=LP2pFYZs} z#G7sJ`&rz`M(i6*SYMTB{DjUZL*`7WlF)lDP&H3uXO?dKFYWCZf$2r@Zb)&k<{2`s zCj+W9DzWgn5*7)zaWH-h7N+8lhpW^|imi2|)$h zPgk)7kMoR6+C}QaR00#x6giV)$oSUOV6cTlSe7_0^G9KA_Fl5Lo?8-B8Jt}pq^ZEs zxmn5Ga}jyr!3pTg%3nJ)8xQTN1zW0;1eDzmaKlhGv|RXN4s^ynA9jKoyP-d(m69=} ztmDWf{{sL03j{gPviUh<6jUsr>OQ*c&TDwtwrLec^U0yTXwN{nigVR414dNj18x^f z;`ag>Ahvz^L-7=>(8aHq%}RSj;zKmBm+Jd}G>?5nc$ng+N=UEiqm00ZW4TtsSAzQ2 zF2$#1>Zew19vjk9_7jE=X!msz085y}Gbttar)B(0CqMVd#3M*92_f4?_5vhVL99L7=rl{zbcK56@EVh@ zsuNQ1xBR!$nVw%%cA>x+0^)I0Cxmr;CK+T$60vhpotFpF*2F51VXA-IiJ<|GSuG^D zG7|7B60o-@0JkHHYt%98lp3gipT$c3qH#kFUaLi6>#(SNpyx%=NEEG$+pEzL2S(P! zXlP@sh&IORuB#q6lRpVpx$(YzsJJ&+U0*aN_h@NSbwZM)L}AjxVh!rZt?s8&#N&!U z%Fk}r2Q8Or9K#5SQ7#TpQT?GpT%DqfrQdFLIZ`I%Toc#~uRK zUw+UX7%(|CfWD_FYz#y7`FIaHqSN!4T_|HT>n_>|oCRjDW+ac87B@-aIbKUbE6>Iv zZEGQk7E$F0om85W@6Nn{VF_s+BYJ8ar^P$GR0Caohtm9|uvkn1#B8D7DtDQSnqwLN z$LT<9qPNpHK;$?z4gD(`Fyf&2l_LJ1Z?sY7T&OWF9kI*AOE<`$Ul|q#Js#S>zm;0_ zSZaLhyp9ik+yTUn7OMTw)$H#)ZEEl>fCs~u17c~_gr|8vjJsWiDn->iwMr zQe*DROD@Kp&$dE;i{vhTCcSx)Kyu=aF)~KlTWP8zCpw!9?SLGdb6KsN`&?GSM|(&M z$%dOf2Xndn4g|h16LXb$%rds{nfL*eHWWbyTxRTAW$wQXJ$>`jm?vuw{-cn3_=!Kk z;<)^eM%$(Ts`09=Fhz9wL5Lh5LR?QHAnHKy^US?2EIENH6aG%J=eD?d3rSS-A~7c& zW=D|0@Tlu5G5$Ckfx3mW&+6js;|f~t^|*z2gGcc`Ch$K85R`>=fB zJ7VITxRb3=B`4_FUZJZaxnN+u{N}uqz-rP_F*b-q7GasrcPYnOD&r3C#=65YU{+159b0O)gWRqutRd#&s^Qy@gO} zI0%U%V?Hzgq=4>~cL}p)2Jy`ITxlX%{h{Pn+fQxEk-4osf3aQ2JJws*K62U#T|-P+ z^#TIF+a>VfaIZel6A4v~BSLmtQfmlE3 z1LKYKZ}N!~^`=nUMc3w)zZ6@Fa^u_W?D7N%Lej)khywlScAzITN9;m$qRq_-V-tPam{39}mjk5+-^H=dsF0hTzmZPamL_WNEH^+)X$P96c-0p@>K zSvez2uwERRh(yC0`fWXi@9n*Q-?3zB&Af2h|$q0FvXZyC)dQfT(qO?uVE75FD>sl9EuG%1k ztx9keg3Pc1w(+`5dL+7UCEcEQ@jRo848eFm=vvDSE1>SXZ>P6^GX82Df3CnG&x3rhUQ`f&68GB5j?N2nCS8 f&IlhgBwvtym3O{2U!e)1wo(VEpxyo=6003ZIGJUS literal 0 HcmV?d00001 diff --git a/tests/irot-missing-essential.avif b/tests/irot-missing-essential.avif new file mode 100644 index 0000000000000000000000000000000000000000..8c62e55ecc8c8028fce66de79f1a3e5cbd2ac841 GIT binary patch literal 84837 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 literal 0 HcmV?d00001 diff --git a/tests/multiple-extents.avif b/tests/multiple-extents.avif new file mode 100644 index 0000000000000000000000000000000000000000..dd306992cb3c2f421f98b3e9f56f974102500034 GIT binary patch literal 342 zcmZQzU{FXasVqn=%S>Yc0uY^>nP!-qnV9D5Xy^zO`jwknk_eIm0*#E6oFWL5fuSHX zxdg@r(K(q(Fk|=%GD~v7a*RMyEi)%S8N_p8U|}Wnk!>pOX)^IxDlFr~oMLomr5a52Qshiwjag3V|R6iUkTXD>6X> z%p4pXK#C`^%+R?}0mx!t5Khj|DauREsQ{YH%fQwE6cW!Y$c1?k=s*@mre+qPuuX1C zVu=s~7pH~f#>bhAylfMUR0S9qwghf|oA*Td$1>k+)~{J}PsejDv#`p~o%H)D-~T%c JvtB;m0RYn!M=t;X literal 0 HcmV?d00001 diff --git a/tests/no-mif1.avif b/tests/no-mif1.avif new file mode 100644 index 0000000000000000000000000000000000000000..1ccbcc43fcf0e2367f476bd56830d1491e1b6955 GIT binary patch literal 334 zcmZQzU{FXasVqn=%S>Yc0uWsR1i6`sX}*qzjzFPLxv3?IAUPn=$Vka4f-o5v3Nn*R zU~CYblbHlFhOZ#ABo{2l2qfh)bMlixJQoHAMj!-{42)7h+60I-GBfkQc7tW4fSgn& zpfHG+SY}}8oS%~qwlXWTpr`;S?wwhXoDZZ$GK&jRK?;E&1&RdEXYj+$pHb-VJwVH%`8BsO>RnJi4X%9 zr-kIk$C-@0Y!i%B1sE8%1a5wt_eA-}GT&_0uUT_X$8#;Su*%P!^!q8_|2qq_UOwLe E0BG|^Q~&?~ literal 0 HcmV?d00001 diff --git a/tests/overflow.rs b/tests/overflow.rs new file mode 100644 index 0000000..ce5dc52 --- /dev/null +++ b/tests/overflow.rs @@ -0,0 +1,15 @@ +/// Verify we're built with run-time integer overflow detection. + +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#[test] +#[should_panic(expected = "attempt to add with overflow")] +fn overflow_protection() { + let edge = u32::max_value(); + assert_eq!(0u32, edge + 1); + + let edge = u64::max_value(); + assert_eq!(0u64, edge + 1); +} diff --git a/tests/public.rs b/tests/public.rs new file mode 100644 index 0000000..30e2bff --- /dev/null +++ b/tests/public.rs @@ -0,0 +1,1215 @@ +/// Check if needed fields are still public. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +extern crate mp4parse as mp4; + +use mp4::Error; +use mp4::ParseStrictness; +use std::convert::TryInto; +use std::fs::File; +use std::io::{Cursor, Read, Seek, SeekFrom}; + +static MINI_MP4: &str = "tests/minimal.mp4"; +static MINI_MP4_WITH_METADATA: &str = "tests/metadata.mp4"; +static MINI_MP4_WITH_METADATA_STD_GENRE: &str = "tests/metadata_gnre.mp4"; + +static AUDIO_EME_CENC_MP4: &str = "tests/bipbop-cenc-audioinit.mp4"; +static VIDEO_EME_CENC_MP4: &str = "tests/bipbop_480wp_1001kbps-cenc-video-key1-init.mp4"; +// The cbcs files were created via shaka-packager from Firefox's test suite's bipbop.mp4 using: +// packager-win.exe +// in=bipbop.mp4,stream=audio,init_segment=bipbop_cbcs_audio_init.mp4,segment_template=bipbop_cbcs_audio_$Number$.m4s +// in=bipbop.mp4,stream=video,init_segment=bipbop_cbcs_video_init.mp4,segment_template=bipbop_cbcs_video_$Number$.m4s +// --protection_scheme cbcs --enable_raw_key_encryption +// --keys label=:key_id=7e571d047e571d047e571d047e571d21:key=7e5744447e5744447e5744447e574421 +// --iv 11223344556677889900112233445566 +// --generate_static_mpd --mpd_output bipbop_cbcs.mpd +// note: only the init files are needed for these tests +static AUDIO_EME_CBCS_MP4: &str = "tests/bipbop_cbcs_audio_init.mp4"; +static VIDEO_EME_CBCS_MP4: &str = "tests/bipbop_cbcs_video_init.mp4"; +static VIDEO_AV1_MP4: &str = "tests/tiny_av1.mp4"; +// This file contains invalid userdata in its copyright userdata. See +// https://bugzilla.mozilla.org/show_bug.cgi?id=1687357 for more information. +static VIDEO_INVALID_USERDATA: &str = "tests/invalid_userdata.mp4"; +static IMAGE_AVIF: &str = "tests/valid.avif"; +static IMAGE_AVIF_EXTENTS: &str = "tests/multiple-extents.avif"; +static IMAGE_AVIF_ALPHA: &str = "tests/valid-alpha.avif"; +static IMAGE_AVIF_ALPHA_PREMULTIPLIED: &str = "tests/1x1-black-alpha-50pct-premultiplied.avif"; +static IMAGE_AVIF_CORRUPT: &str = "tests/corrupt/bug-1655846.avif"; +static IMAGE_AVIF_CORRUPT_2: &str = "tests/corrupt/bug-1661347.avif"; +static IMAGE_AVIF_IPMA_BAD_VERSION: &str = "tests/bad-ipma-version.avif"; +static IMAGE_AVIF_IPMA_BAD_FLAGS: &str = "tests/bad-ipma-flags.avif"; +static IMAGE_AVIF_IPMA_DUPLICATE_VERSION_AND_FLAGS: &str = + "tests/corrupt/ipma-duplicate-version-and-flags.avif"; +static IMAGE_AVIF_IPMA_DUPLICATE_ITEM_ID: &str = "tests/corrupt/ipma-duplicate-item_id.avif"; +static IMAGE_AVIF_IPMA_INVALID_PROPERTY_INDEX: &str = + "tests/corrupt/ipma-invalid-property-index.avif"; +static IMAGE_AVIF_NO_HDLR: &str = "tests/corrupt/hdlr-not-first.avif"; +static IMAGE_AVIF_HDLR_NOT_FIRST: &str = "tests/corrupt/no-hdlr.avif"; +static IMAGE_AVIF_HDLR_NOT_PICT: &str = "tests/corrupt/hdlr-not-pict.avif"; +static IMAGE_AVIF_HDLR_NONZERO_RESERVED: &str = "tests/hdlr-nonzero-reserved.avif"; +static IMAGE_AVIF_NO_MIF1: &str = "tests/no-mif1.avif"; +static IMAGE_AVIF_NO_PIXI: &str = "tests/corrupt/no-pixi.avif"; +static IMAGE_AVIF_NO_AV1C: &str = "tests/corrupt/no-av1C.avif"; +static IMAGE_AVIF_NO_ISPE: &str = "tests/corrupt/no-ispe.avif"; +static IMAGE_AVIF_NO_ALPHA_AV1C: &str = "tests/corrupt/no-alpha-av1C.avif"; +static IMAGE_AVIF_NO_ALPHA_PIXI: &str = "tests/corrupt/no-pixi-for-alpha.avif"; +static IMAGE_AVIF_AV1C_MISSING_ESSENTIAL: &str = "tests/av1C-missing-essential.avif"; +static IMAGE_AVIF_IMIR_MISSING_ESSENTIAL: &str = "tests/imir-missing-essential.avif"; +static IMAGE_AVIF_IROT_MISSING_ESSENTIAL: &str = "tests/irot-missing-essential.avif"; +static AVIF_TEST_DIRS: &[&str] = &["tests", "av1-avif/testFiles"]; +static AVIF_A1OP: &str = + "av1-avif/testFiles/Apple/multilayer_examples/animals_00_multilayer_a1op.avif"; +static AVIF_A1LX: &str = + "av1-avif/testFiles/Apple/multilayer_examples/animals_00_multilayer_grid_a1lx.avif"; +static AVIF_CLAP: &str = "tests/clap-basic-1_3x3-to-1x1.avif"; +static AVIF_GRID: &str = "av1-avif/testFiles/Microsoft/Summer_in_Tomsk_720p_5x4_grid.avif"; +static AVIF_LSEL: &str = + "av1-avif/testFiles/Apple/multilayer_examples/animals_00_multilayer_lsel.avif"; +static AVIF_NO_PIXI_IMAGES: &[&str] = &[IMAGE_AVIF_NO_PIXI, IMAGE_AVIF_NO_ALPHA_PIXI]; +static AVIF_UNSUPPORTED_IMAGES: &[&str] = &[ + AVIF_A1OP, + AVIF_A1LX, + AVIF_CLAP, + AVIF_GRID, + AVIF_LSEL, + "av1-avif/testFiles/Apple/multilayer_examples/animals_00_multilayer_a1lx.avif", + "av1-avif/testFiles/Apple/multilayer_examples/animals_00_multilayer_a1op_lsel.avif", + "av1-avif/testFiles/Apple/multilayer_examples/animals_00_multilayer_grid_lsel.avif", + "av1-avif/testFiles/Xiph/abandoned_filmgrain.avif", + "av1-avif/testFiles/Xiph/fruits_2layer_thumbsize.avif", + "av1-avif/testFiles/Xiph/quebec_3layer_op2.avif", + "av1-avif/testFiles/Xiph/tiger_3layer_1res.avif", + "av1-avif/testFiles/Xiph/tiger_3layer_3res.avif", +]; +/// See https://github.com/AOMediaCodec/av1-avif/issues/150 +static AV1_AVIF_CORRUPT_IMAGES: &[&str] = &[ + "av1-avif/testFiles/Microsoft/Chimera_10bit_cropped_to_1920x1008.avif", + "av1-avif/testFiles/Microsoft/Chimera_10bit_cropped_to_1920x1008_with_HDR_metadata.avif", + "av1-avif/testFiles/Microsoft/Chimera_8bit_cropped_480x256.avif", + "av1-avif/testFiles/Link-U/kimono.crop.avif", + "av1-avif/testFiles/Link-U/kimono.mirror-vertical.rotate270.crop.avif", + "av1-avif/testFiles/Netflix/avis/Chimera-AV1-10bit-480x270.avif", +]; +static AVIF_CORRUPT_IMAGES_DIR: &str = "tests/corrupt"; +// The 1 frame h263 3gp file can be generated by ffmpeg with command +// "ffmpeg -i [input file] -f 3gp -vcodec h263 -vf scale=176x144 -frames:v 1 -an output.3gp" +static VIDEO_H263_3GP: &str = "tests/bbb_sunflower_QCIF_30fps_h263_noaudio_1f.3gp"; +// The 1 frame AMR-NB 3gp file can be generated by ffmpeg with command +// "ffmpeg -i [input file] -f 3gp -acodec amr_nb -ar 8000 -ac 1 -frames:a 1 -vn output.3gp" +#[cfg(feature = "3gpp")] +static AUDIO_AMRNB_3GP: &str = "tests/amr_nb_1f.3gp"; +// The 1 frame AMR-WB 3gp file can be generated by ffmpeg with command +// "ffmpeg -i [input file] -f 3gp -acodec amr_wb -ar 16000 -ac 1 -frames:a 1 -vn output.3gp" +#[cfg(feature = "3gpp")] +static AUDIO_AMRWB_3GP: &str = "tests/amr_wb_1f.3gp"; +#[cfg(feature = "mp4v")] +// The 1 frame mp4v mp4 file can be generated by ffmpeg with command +// "ffmpeg -i [input file] -f mp4 -c:v mpeg4 -vf scale=176x144 -frames:v 1 -an output.mp4" +static VIDEO_MP4V_MP4: &str = "tests/bbb_sunflower_QCIF_30fps_mp4v_noaudio_1f.mp4"; + +// Adapted from https://github.com/GuillaumeGomez/audio-video-metadata/blob/9dff40f565af71d5502e03a2e78ae63df95cfd40/src/metadata.rs#L53 +#[test] +fn public_api() { + let mut fd = File::open(MINI_MP4).expect("Unknown file"); + let mut buf = Vec::new(); + fd.read_to_end(&mut buf).expect("File error"); + + let mut c = Cursor::new(&buf); + let context = mp4::read_mp4(&mut c).expect("read_mp4 failed"); + assert_eq!(context.timescale, Some(mp4::MediaTimeScale(1000))); + for track in context.tracks { + match track.track_type { + mp4::TrackType::Video => { + // track part + assert_eq!(track.duration, Some(mp4::TrackScaledTime(512, 0))); + assert_eq!(track.empty_duration, Some(mp4::MediaScaledTime(0))); + assert_eq!(track.media_time, Some(mp4::TrackScaledTime(0, 0))); + assert_eq!(track.timescale, Some(mp4::TrackTimeScale(12800, 0))); + + // track.tkhd part + let tkhd = track.tkhd.unwrap(); + assert!(!tkhd.disabled); + assert_eq!(tkhd.duration, 40); + assert_eq!(tkhd.width, 20_971_520); + assert_eq!(tkhd.height, 15_728_640); + + // track.stsd part + let stsd = track.stsd.expect("expected an stsd"); + let v = match stsd.descriptions.first().expect("expected a SampleEntry") { + mp4::SampleEntry::Video(v) => v, + _ => panic!("expected a VideoSampleEntry"), + }; + assert_eq!(v.width, 320); + assert_eq!(v.height, 240); + assert_eq!( + match v.codec_specific { + mp4::VideoCodecSpecific::AVCConfig(ref avc) => { + assert!(!avc.is_empty()); + "AVC" + } + mp4::VideoCodecSpecific::VPxConfig(ref vpx) => { + // We don't enter in here, we just check if fields are public. + assert!(vpx.bit_depth > 0); + assert!(vpx.colour_primaries > 0); + assert!(vpx.chroma_subsampling > 0); + assert!(!vpx.codec_init.is_empty()); + "VPx" + } + mp4::VideoCodecSpecific::ESDSConfig(ref mp4v) => { + assert!(!mp4v.is_empty()); + "MP4V" + } + mp4::VideoCodecSpecific::AV1Config(ref _av1c) => { + "AV1" + } + mp4::VideoCodecSpecific::H263Config(ref _h263) => { + "H263" + } + }, + "AVC" + ); + } + mp4::TrackType::Audio => { + // track part + assert_eq!(track.duration, Some(mp4::TrackScaledTime(2944, 1))); + assert_eq!(track.empty_duration, Some(mp4::MediaScaledTime(0))); + assert_eq!(track.media_time, Some(mp4::TrackScaledTime(1024, 1))); + assert_eq!(track.timescale, Some(mp4::TrackTimeScale(48000, 1))); + + // track.tkhd part + let tkhd = track.tkhd.unwrap(); + assert!(!tkhd.disabled); + assert_eq!(tkhd.duration, 62); + assert_eq!(tkhd.width, 0); + assert_eq!(tkhd.height, 0); + + // track.stsd part + let stsd = track.stsd.expect("expected an stsd"); + let a = match stsd.descriptions.first().expect("expected a SampleEntry") { + mp4::SampleEntry::Audio(a) => a, + _ => panic!("expected a AudioSampleEntry"), + }; + assert_eq!( + match a.codec_specific { + mp4::AudioCodecSpecific::ES_Descriptor(ref esds) => { + assert_eq!(esds.audio_codec, mp4::CodecType::AAC); + assert_eq!(esds.audio_sample_rate.unwrap(), 48000); + assert_eq!(esds.audio_object_type.unwrap(), 2); + "ES" + } + mp4::AudioCodecSpecific::FLACSpecificBox(ref flac) => { + // STREAMINFO block must be present and first. + assert!(!flac.blocks.is_empty()); + assert_eq!(flac.blocks[0].block_type, 0); + assert_eq!(flac.blocks[0].data.len(), 34); + "FLAC" + } + mp4::AudioCodecSpecific::OpusSpecificBox(ref opus) => { + // We don't enter in here, we just check if fields are public. + assert!(opus.version > 0); + "Opus" + } + mp4::AudioCodecSpecific::ALACSpecificBox(ref alac) => { + assert!(alac.data.len() == 24 || alac.data.len() == 48); + "ALAC" + } + mp4::AudioCodecSpecific::MP3 => { + "MP3" + } + mp4::AudioCodecSpecific::LPCM => { + "LPCM" + } + #[cfg(feature = "3gpp")] + mp4::AudioCodecSpecific::AMRSpecificBox(_) => { + "AMR" + } + }, + "ES" + ); + assert!(a.samplesize > 0); + assert!(a.samplerate > 0.0); + } + mp4::TrackType::Metadata | mp4::TrackType::Unknown => {} + } + } +} + +#[test] +fn public_metadata() { + let mut fd = File::open(MINI_MP4_WITH_METADATA).expect("Unknown file"); + let mut buf = Vec::new(); + fd.read_to_end(&mut buf).expect("File error"); + + let mut c = Cursor::new(&buf); + let context = mp4::read_mp4(&mut c).expect("read_mp4 failed"); + let udta = context + .userdata + .expect("didn't find udta") + .expect("failed to parse udta"); + let meta = udta.meta.expect("didn't find meta"); + assert_eq!(meta.title.unwrap(), "Title"); + assert_eq!(meta.artist.unwrap(), "Artist"); + assert_eq!(meta.album_artist.unwrap(), "Album Artist"); + assert_eq!(meta.comment.unwrap(), "Comments"); + assert_eq!(meta.year.unwrap(), "2019"); + assert_eq!( + meta.genre.unwrap(), + mp4::Genre::CustomGenre("Custom Genre".try_into().unwrap()) + ); + assert_eq!(meta.encoder.unwrap(), "Lavf56.40.101"); + assert_eq!(meta.encoded_by.unwrap(), "Encoded-by"); + assert_eq!(meta.copyright.unwrap(), "Copyright"); + assert_eq!(meta.track_number.unwrap(), 3); + assert_eq!(meta.total_tracks.unwrap(), 6); + assert_eq!(meta.disc_number.unwrap(), 5); + assert_eq!(meta.total_discs.unwrap(), 10); + assert_eq!(meta.beats_per_minute.unwrap(), 128); + assert_eq!(meta.composer.unwrap(), "Composer"); + assert!(meta.compilation.unwrap()); + assert!(!meta.gapless_playback.unwrap()); + assert!(!meta.podcast.unwrap()); + assert_eq!(meta.advisory.unwrap(), mp4::AdvisoryRating::Clean); + assert_eq!(meta.media_type.unwrap(), mp4::MediaType::Normal); + assert_eq!(meta.rating.unwrap(), "50"); + assert_eq!(meta.grouping.unwrap(), "Grouping"); + assert_eq!(meta.category.unwrap(), "Category"); + assert_eq!(meta.keyword.unwrap(), "Keyword"); + assert_eq!(meta.description.unwrap(), "Description"); + assert_eq!(meta.lyrics.unwrap(), "Lyrics"); + assert_eq!(meta.long_description.unwrap(), "Long Description"); + assert_eq!(meta.tv_episode_name.unwrap(), "Episode Name"); + assert_eq!(meta.tv_network_name.unwrap(), "Network Name"); + assert_eq!(meta.tv_episode_number.unwrap(), 15); + assert_eq!(meta.tv_season.unwrap(), 10); + assert_eq!(meta.tv_show_name.unwrap(), "Show Name"); + assert!(meta.hd_video.unwrap()); + assert_eq!(meta.owner.unwrap(), "Owner"); + assert_eq!(meta.sort_name.unwrap(), "Sort Name"); + assert_eq!(meta.sort_album.unwrap(), "Sort Album"); + assert_eq!(meta.sort_artist.unwrap(), "Sort Artist"); + assert_eq!(meta.sort_album_artist.unwrap(), "Sort Album Artist"); + assert_eq!(meta.sort_composer.unwrap(), "Sort Composer"); + + // Check for valid JPEG header + let covers = meta.cover_art.unwrap(); + let cover = &covers[0]; + let mut bytes = [0u8; 4]; + bytes[0] = cover[0]; + bytes[1] = cover[1]; + bytes[2] = cover[2]; + assert_eq!(u32::from_le_bytes(bytes), 0x00ff_d8ff); +} + +#[test] +fn public_metadata_gnre() { + let mut fd = File::open(MINI_MP4_WITH_METADATA_STD_GENRE).expect("Unknown file"); + let mut buf = Vec::new(); + fd.read_to_end(&mut buf).expect("File error"); + + let mut c = Cursor::new(&buf); + let context = mp4::read_mp4(&mut c).expect("read_mp4 failed"); + let udta = context + .userdata + .expect("didn't find udta") + .expect("failed to parse udta"); + let meta = udta.meta.expect("didn't find meta"); + assert_eq!(meta.title.unwrap(), "Title"); + assert_eq!(meta.artist.unwrap(), "Artist"); + assert_eq!(meta.album_artist.unwrap(), "Album Artist"); + assert_eq!(meta.comment.unwrap(), "Comments"); + assert_eq!(meta.year.unwrap(), "2019"); + assert_eq!(meta.genre.unwrap(), mp4::Genre::StandardGenre(3)); + assert_eq!(meta.encoder.unwrap(), "Lavf56.40.101"); + assert_eq!(meta.encoded_by.unwrap(), "Encoded-by"); + assert_eq!(meta.copyright.unwrap(), "Copyright"); + assert_eq!(meta.track_number.unwrap(), 3); + assert_eq!(meta.total_tracks.unwrap(), 6); + assert_eq!(meta.disc_number.unwrap(), 5); + assert_eq!(meta.total_discs.unwrap(), 10); + assert_eq!(meta.beats_per_minute.unwrap(), 128); + assert_eq!(meta.composer.unwrap(), "Composer"); + assert!(meta.compilation.unwrap()); + assert!(!meta.gapless_playback.unwrap()); + assert!(!meta.podcast.unwrap()); + assert_eq!(meta.advisory.unwrap(), mp4::AdvisoryRating::Clean); + assert_eq!(meta.media_type.unwrap(), mp4::MediaType::Normal); + assert_eq!(meta.rating.unwrap(), "50"); + assert_eq!(meta.grouping.unwrap(), "Grouping"); + assert_eq!(meta.category.unwrap(), "Category"); + assert_eq!(meta.keyword.unwrap(), "Keyword"); + assert_eq!(meta.description.unwrap(), "Description"); + assert_eq!(meta.lyrics.unwrap(), "Lyrics"); + assert_eq!(meta.long_description.unwrap(), "Long Description"); + assert_eq!(meta.tv_episode_name.unwrap(), "Episode Name"); + assert_eq!(meta.tv_network_name.unwrap(), "Network Name"); + assert_eq!(meta.tv_episode_number.unwrap(), 15); + assert_eq!(meta.tv_season.unwrap(), 10); + assert_eq!(meta.tv_show_name.unwrap(), "Show Name"); + assert!(meta.hd_video.unwrap()); + assert_eq!(meta.owner.unwrap(), "Owner"); + assert_eq!(meta.sort_name.unwrap(), "Sort Name"); + assert_eq!(meta.sort_album.unwrap(), "Sort Album"); + assert_eq!(meta.sort_artist.unwrap(), "Sort Artist"); + assert_eq!(meta.sort_album_artist.unwrap(), "Sort Album Artist"); + assert_eq!(meta.sort_composer.unwrap(), "Sort Composer"); + + // Check for valid JPEG header + let covers = meta.cover_art.unwrap(); + let cover = &covers[0]; + let mut bytes = [0u8; 4]; + bytes[0] = cover[0]; + bytes[1] = cover[1]; + bytes[2] = cover[2]; + assert_eq!(u32::from_le_bytes(bytes), 0x00ff_d8ff); +} + +#[test] +fn public_invalid_metadata() { + // Test that reading userdata containing invalid metadata is not fatal to parsing and that + // expected values are still found. + let mut fd = File::open(VIDEO_INVALID_USERDATA).expect("Unknown file"); + let mut buf = Vec::new(); + fd.read_to_end(&mut buf).expect("File error"); + + let mut c = Cursor::new(&buf); + let context = mp4::read_mp4(&mut c).expect("read_mp4 failed"); + // Should have userdata. + assert!(context.userdata.is_some()); + // But it should contain an error. + assert!(context.userdata.unwrap().is_err()); + // Smoke test that other data has been parsed. Don't check everything, just make sure some + // values are as expected. + assert_eq!(context.tracks.len(), 2); + for track in context.tracks { + match track.track_type { + mp4::TrackType::Video => { + // Check some of the values in the video tkhd. + let tkhd = track.tkhd.unwrap(); + assert!(!tkhd.disabled); + assert_eq!(tkhd.duration, 231232); + assert_eq!(tkhd.width, 83_886_080); + assert_eq!(tkhd.height, 47_185_920); + } + mp4::TrackType::Audio => { + // Check some of the values in the audio tkhd. + let tkhd = track.tkhd.unwrap(); + assert!(!tkhd.disabled); + assert_eq!(tkhd.duration, 231338); + assert_eq!(tkhd.width, 0); + assert_eq!(tkhd.height, 0); + } + _ => panic!("File should not contain other tracks."), + } + } +} + +#[test] +fn public_audio_tenc() { + let kid = vec![ + 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, + 0x04, + ]; + + let mut fd = File::open(AUDIO_EME_CENC_MP4).expect("Unknown file"); + let mut buf = Vec::new(); + fd.read_to_end(&mut buf).expect("File error"); + + let mut c = Cursor::new(&buf); + let context = mp4::read_mp4(&mut c).expect("read_mp4 failed"); + for track in context.tracks { + let stsd = track.stsd.expect("expected an stsd"); + let a = match stsd.descriptions.first().expect("expected a SampleEntry") { + mp4::SampleEntry::Audio(a) => a, + _ => panic!("expected a AudioSampleEntry"), + }; + assert_eq!(a.codec_type, mp4::CodecType::EncryptedAudio); + match a.protection_info.iter().find(|sinf| sinf.tenc.is_some()) { + Some(p) => { + assert_eq!(p.original_format, b"mp4a"); + if let Some(ref schm) = p.scheme_type { + assert_eq!(schm.scheme_type, b"cenc"); + } else { + panic!("Expected scheme type info"); + } + if let Some(ref tenc) = p.tenc { + assert!(tenc.is_encrypted > 0); + assert_eq!(tenc.iv_size, 16); + assert_eq!(tenc.kid, kid); + assert_eq!(tenc.crypt_byte_block_count, None); + assert_eq!(tenc.skip_byte_block_count, None); + assert_eq!(tenc.constant_iv, None); + } else { + panic!("Invalid test condition"); + } + } + _ => { + panic!("Invalid test condition"); + } + } + } +} + +#[test] +fn public_video_cenc() { + let system_id = vec![ + 0x10, 0x77, 0xef, 0xec, 0xc0, 0xb2, 0x4d, 0x02, 0xac, 0xe3, 0x3c, 0x1e, 0x52, 0xe2, 0xfb, + 0x4b, + ]; + + let kid = vec![ + 0x7e, 0x57, 0x1d, 0x03, 0x7e, 0x57, 0x1d, 0x03, 0x7e, 0x57, 0x1d, 0x03, 0x7e, 0x57, 0x1d, + 0x11, + ]; + + let pssh_box = vec![ + 0x00, 0x00, 0x00, 0x34, 0x70, 0x73, 0x73, 0x68, 0x01, 0x00, 0x00, 0x00, 0x10, 0x77, 0xef, + 0xec, 0xc0, 0xb2, 0x4d, 0x02, 0xac, 0xe3, 0x3c, 0x1e, 0x52, 0xe2, 0xfb, 0x4b, 0x00, 0x00, + 0x00, 0x01, 0x7e, 0x57, 0x1d, 0x03, 0x7e, 0x57, 0x1d, 0x03, 0x7e, 0x57, 0x1d, 0x03, 0x7e, + 0x57, 0x1d, 0x11, 0x00, 0x00, 0x00, 0x00, + ]; + + let mut fd = File::open(VIDEO_EME_CENC_MP4).expect("Unknown file"); + let mut buf = Vec::new(); + fd.read_to_end(&mut buf).expect("File error"); + + let mut c = Cursor::new(&buf); + let context = mp4::read_mp4(&mut c).expect("read_mp4 failed"); + for track in context.tracks { + let stsd = track.stsd.expect("expected an stsd"); + let v = match stsd.descriptions.first().expect("expected a SampleEntry") { + mp4::SampleEntry::Video(ref v) => v, + _ => panic!("expected a VideoSampleEntry"), + }; + assert_eq!(v.codec_type, mp4::CodecType::EncryptedVideo); + match v.protection_info.iter().find(|sinf| sinf.tenc.is_some()) { + Some(p) => { + assert_eq!(p.original_format, b"avc1"); + if let Some(ref schm) = p.scheme_type { + assert_eq!(schm.scheme_type, b"cenc"); + } else { + panic!("Expected scheme type info"); + } + if let Some(ref tenc) = p.tenc { + assert!(tenc.is_encrypted > 0); + assert_eq!(tenc.iv_size, 16); + assert_eq!(tenc.kid, kid); + assert_eq!(tenc.crypt_byte_block_count, None); + assert_eq!(tenc.skip_byte_block_count, None); + assert_eq!(tenc.constant_iv, None); + } else { + panic!("Invalid test condition"); + } + } + _ => { + panic!("Invalid test condition"); + } + } + } + + for pssh in context.psshs { + assert_eq!(pssh.system_id, system_id); + for kid_id in pssh.kid { + assert_eq!(kid_id, kid); + } + assert!(pssh.data.is_empty()); + assert_eq!(pssh.box_content, pssh_box); + } +} + +#[test] +fn public_audio_cbcs() { + let system_id = vec![ + 0x10, 0x77, 0xef, 0xec, 0xc0, 0xb2, 0x4d, 0x02, 0xac, 0xe3, 0x3c, 0x1e, 0x52, 0xe2, 0xfb, + 0x4b, + ]; + + let kid = vec![ + 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, + 0x21, + ]; + + let default_iv = vec![ + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, + 0x66, + ]; + + let pssh_box = vec![ + 0x00, 0x00, 0x00, 0x34, 0x70, 0x73, 0x73, 0x68, 0x01, 0x00, 0x00, 0x00, 0x10, 0x77, 0xef, + 0xec, 0xc0, 0xb2, 0x4d, 0x02, 0xac, 0xe3, 0x3c, 0x1e, 0x52, 0xe2, 0xfb, 0x4b, 0x00, 0x00, + 0x00, 0x01, 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x04, 0x7e, + 0x57, 0x1d, 0x21, 0x00, 0x00, 0x00, 0x00, + ]; + + let mut fd = File::open(AUDIO_EME_CBCS_MP4).expect("Unknown file"); + let mut buf = Vec::new(); + fd.read_to_end(&mut buf).expect("File error"); + + let mut c = Cursor::new(&buf); + let context = mp4::read_mp4(&mut c).expect("read_mp4 failed"); + for track in context.tracks { + let stsd = track.stsd.expect("expected an stsd"); + assert_eq!(stsd.descriptions.len(), 2); + let mut found_encrypted_sample_description = false; + for description in stsd.descriptions { + match description { + mp4::SampleEntry::Audio(ref a) => { + if let Some(p) = a.protection_info.iter().find(|sinf| sinf.tenc.is_some()) { + found_encrypted_sample_description = true; + assert_eq!(p.original_format, b"mp4a"); + if let Some(ref schm) = p.scheme_type { + assert_eq!(schm.scheme_type, b"cbcs"); + } else { + panic!("Expected scheme type info"); + } + if let Some(ref tenc) = p.tenc { + assert!(tenc.is_encrypted > 0); + assert_eq!(tenc.iv_size, 0); + assert_eq!(tenc.kid, kid); + // Note: 0 for both crypt and skip seems odd but + // that's what shaka-packager produced. It appears + // to indicate full encryption. + assert_eq!(tenc.crypt_byte_block_count, Some(0)); + assert_eq!(tenc.skip_byte_block_count, Some(0)); + assert_eq!(tenc.constant_iv, Some(default_iv.clone().into())); + } else { + panic!("Invalid test condition"); + } + } + } + _ => { + panic!("expected a VideoSampleEntry"); + } + } + } + assert!( + found_encrypted_sample_description, + "Should have found an encrypted sample description" + ); + } + + for pssh in context.psshs { + assert_eq!(pssh.system_id, system_id); + for kid_id in pssh.kid { + assert_eq!(kid_id, kid); + } + assert!(pssh.data.is_empty()); + assert_eq!(pssh.box_content, pssh_box); + } +} + +#[test] +fn public_video_cbcs() { + let system_id = vec![ + 0x10, 0x77, 0xef, 0xec, 0xc0, 0xb2, 0x4d, 0x02, 0xac, 0xe3, 0x3c, 0x1e, 0x52, 0xe2, 0xfb, + 0x4b, + ]; + + let kid = vec![ + 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, + 0x21, + ]; + + let default_iv = vec![ + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, + 0x66, + ]; + + let pssh_box = vec![ + 0x00, 0x00, 0x00, 0x34, 0x70, 0x73, 0x73, 0x68, 0x01, 0x00, 0x00, 0x00, 0x10, 0x77, 0xef, + 0xec, 0xc0, 0xb2, 0x4d, 0x02, 0xac, 0xe3, 0x3c, 0x1e, 0x52, 0xe2, 0xfb, 0x4b, 0x00, 0x00, + 0x00, 0x01, 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x04, 0x7e, + 0x57, 0x1d, 0x21, 0x00, 0x00, 0x00, 0x00, + ]; + + let mut fd = File::open(VIDEO_EME_CBCS_MP4).expect("Unknown file"); + let mut buf = Vec::new(); + fd.read_to_end(&mut buf).expect("File error"); + + let mut c = Cursor::new(&buf); + let context = mp4::read_mp4(&mut c).expect("read_mp4 failed"); + for track in context.tracks { + let stsd = track.stsd.expect("expected an stsd"); + assert_eq!(stsd.descriptions.len(), 2); + let mut found_encrypted_sample_description = false; + for description in stsd.descriptions { + match description { + mp4::SampleEntry::Video(ref v) => { + assert_eq!(v.width, 400); + assert_eq!(v.height, 300); + if let Some(p) = v.protection_info.iter().find(|sinf| sinf.tenc.is_some()) { + found_encrypted_sample_description = true; + assert_eq!(p.original_format, b"avc1"); + if let Some(ref schm) = p.scheme_type { + assert_eq!(schm.scheme_type, b"cbcs"); + } else { + panic!("Expected scheme type info"); + } + if let Some(ref tenc) = p.tenc { + assert!(tenc.is_encrypted > 0); + assert_eq!(tenc.iv_size, 0); + assert_eq!(tenc.kid, kid); + assert_eq!(tenc.crypt_byte_block_count, Some(1)); + assert_eq!(tenc.skip_byte_block_count, Some(9)); + assert_eq!(tenc.constant_iv, Some(default_iv.clone().into())); + } else { + panic!("Invalid test condition"); + } + } + } + _ => { + panic!("expected a VideoSampleEntry"); + } + } + } + assert!( + found_encrypted_sample_description, + "Should have found an encrypted sample description" + ); + } + + for pssh in context.psshs { + assert_eq!(pssh.system_id, system_id); + for kid_id in pssh.kid { + assert_eq!(kid_id, kid); + } + assert!(pssh.data.is_empty()); + assert_eq!(pssh.box_content, pssh_box); + } +} + +#[test] +fn public_video_av1() { + let mut fd = File::open(VIDEO_AV1_MP4).expect("Unknown file"); + let mut buf = Vec::new(); + fd.read_to_end(&mut buf).expect("File error"); + + let mut c = Cursor::new(&buf); + let context = mp4::read_mp4(&mut c).expect("read_mp4 failed"); + for track in context.tracks { + // track part + assert_eq!(track.duration, Some(mp4::TrackScaledTime(512, 0))); + assert_eq!(track.empty_duration, Some(mp4::MediaScaledTime(0))); + assert_eq!(track.media_time, Some(mp4::TrackScaledTime(0, 0))); + assert_eq!(track.timescale, Some(mp4::TrackTimeScale(12288, 0))); + + // track.tkhd part + let tkhd = track.tkhd.unwrap(); + assert!(!tkhd.disabled); + assert_eq!(tkhd.duration, 42); + assert_eq!(tkhd.width, 4_194_304); + assert_eq!(tkhd.height, 4_194_304); + + // track.stsd part + let stsd = track.stsd.expect("expected an stsd"); + let v = match stsd.descriptions.first().expect("expected a SampleEntry") { + mp4::SampleEntry::Video(ref v) => v, + _ => panic!("expected a VideoSampleEntry"), + }; + assert_eq!(v.codec_type, mp4::CodecType::AV1); + assert_eq!(v.width, 64); + assert_eq!(v.height, 64); + + match v.codec_specific { + mp4::VideoCodecSpecific::AV1Config(ref av1c) => { + // TODO: test av1c fields once ffmpeg is updated + assert_eq!(av1c.profile, 0); + assert_eq!(av1c.level, 0); + assert_eq!(av1c.tier, 0); + assert_eq!(av1c.bit_depth, 8); + assert!(!av1c.monochrome); + assert_eq!(av1c.chroma_subsampling_x, 1); + assert_eq!(av1c.chroma_subsampling_y, 1); + assert_eq!(av1c.chroma_sample_position, 0); + assert!(!av1c.initial_presentation_delay_present); + assert_eq!(av1c.initial_presentation_delay_minus_one, 0); + } + _ => panic!("Invalid test condition"), + } + } +} + +#[test] +fn public_mp4_bug_1185230() { + let input = &mut File::open("tests/test_case_1185230.mp4").expect("Unknown file"); + let context = mp4::read_mp4(input).expect("read_mp4 failed"); + let number_video_tracks = context + .tracks + .iter() + .filter(|t| t.track_type == mp4::TrackType::Video) + .count(); + let number_audio_tracks = context + .tracks + .iter() + .filter(|t| t.track_type == mp4::TrackType::Audio) + .count(); + assert_eq!(number_video_tracks, 2); + assert_eq!(number_audio_tracks, 2); +} + +#[test] +fn public_mp4_ctts_overflow() { + let input = &mut File::open("tests/clusterfuzz-testcase-minimized-mp4-6093954524250112") + .expect("Unknown file"); + assert_invalid_data(mp4::read_mp4(input), "insufficient data in 'ctts' box"); +} + +#[test] +fn public_avif_primary_item() { + let input = &mut File::open(IMAGE_AVIF).expect("Unknown file"); + let context = mp4::read_avif(input, ParseStrictness::Normal).expect("read_avif failed"); + assert_eq!( + context.primary_item_coded_data(), + [ + 0x12, 0x00, 0x0A, 0x07, 0x38, 0x00, 0x06, 0x90, 0x20, 0x20, 0x69, 0x32, 0x0C, 0x16, + 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x79, 0x4C, 0xD2, 0x02 + ] + ); +} + +#[test] +fn public_avif_primary_item_split_extents() { + let input = &mut File::open(IMAGE_AVIF_EXTENTS).expect("Unknown file"); + let context = mp4::read_avif(input, ParseStrictness::Normal).expect("read_avif failed"); + assert_eq!(context.primary_item_coded_data().len(), 52); +} + +#[test] +fn public_avif_alpha_item() { + let input = &mut File::open(IMAGE_AVIF_ALPHA).expect("Unknown file"); + assert_avif_valid(input); +} + +#[test] +fn public_avif_alpha_non_premultiplied() { + let input = &mut File::open(IMAGE_AVIF_ALPHA).expect("Unknown file"); + let context = mp4::read_avif(input, ParseStrictness::Normal).expect("read_avif failed"); + assert!(!context.alpha_item_coded_data().is_empty()); + assert!(!context.premultiplied_alpha); +} + +#[test] +fn public_avif_alpha_premultiplied() { + let input = &mut File::open(IMAGE_AVIF_ALPHA_PREMULTIPLIED).expect("Unknown file"); + let context = mp4::read_avif(input, ParseStrictness::Normal).expect("read_avif failed"); + assert!(!context.alpha_item_coded_data().is_empty()); + assert!(context.premultiplied_alpha); + assert_avif_valid(input); +} + +#[test] +fn public_avif_bug_1655846() { + let input = &mut File::open(IMAGE_AVIF_CORRUPT).expect("Unknown file"); + assert!(mp4::read_avif(input, ParseStrictness::Normal).is_err()); +} + +#[test] +fn public_avif_bug_1661347() { + let input = &mut File::open(IMAGE_AVIF_CORRUPT_2).expect("Unknown file"); + assert!(mp4::read_avif(input, ParseStrictness::Normal).is_err()); +} + +fn assert_invalid_data(result: mp4::Result, expected_msg: &str) { + match result { + Err(Error::InvalidData(msg)) if msg == expected_msg => {} + Err(Error::InvalidData(msg)) if msg != expected_msg => { + panic!( + "Error message mismatch\nExpected: {}\nFound: {}", + expected_msg, msg + ); + } + r => panic!( + "Expected Err(Error::InvalidData({:?}), found {:?}", + expected_msg, r + ), + } +} + +/// Check that input generates no errors in any parsing mode +fn assert_avif_valid(input: &mut File) { + for strictness in &[ + ParseStrictness::Permissive, + ParseStrictness::Normal, + ParseStrictness::Strict, + ] { + input.seek(SeekFrom::Start(0)).expect("rewind failed"); + assert!( + mp4::read_avif(input, *strictness).is_ok(), + "read_avif with {:?} failed", + strictness + ); + println!("{:?} succeeded", strictness); + } +} + +/// Check that input generates the expected error only in strict parsing mode +fn assert_avif_should(path: &str, expected_msg: &str) { + let input = &mut File::open(path).expect("Unknown file"); + assert_invalid_data(mp4::read_avif(input, ParseStrictness::Strict), expected_msg); + input.seek(SeekFrom::Start(0)).expect("rewind failed"); + mp4::read_avif(input, ParseStrictness::Normal).expect("ParseStrictness::Normal failed"); + input.seek(SeekFrom::Start(0)).expect("rewind failed"); + mp4::read_avif(input, ParseStrictness::Permissive).expect("ParseStrictness::Permissive failed"); +} + +/// Check that input generates the expected error unless in permissive parsing mode +fn assert_avif_shall(path: &str, expected_msg: &str) { + let input = &mut File::open(path).expect("Unknown file"); + assert_invalid_data(mp4::read_avif(input, ParseStrictness::Strict), expected_msg); + input.seek(SeekFrom::Start(0)).expect("rewind failed"); + assert_invalid_data(mp4::read_avif(input, ParseStrictness::Normal), expected_msg); + input.seek(SeekFrom::Start(0)).expect("rewind failed"); + mp4::read_avif(input, ParseStrictness::Permissive).expect("ParseStrictness::Permissive failed"); +} + +#[test] +fn public_avif_ipma_missing_essential() { + let expected_msg = "All transformative properties associated with \ + coded and derived images required or conditionally \ + required by this document shall be marked as essential \ + per MIAF (ISO 23000-22:2019) § 7.3.9"; + assert_avif_should(IMAGE_AVIF_AV1C_MISSING_ESSENTIAL, expected_msg); + assert_avif_should(IMAGE_AVIF_IMIR_MISSING_ESSENTIAL, expected_msg); + assert_avif_should(IMAGE_AVIF_IROT_MISSING_ESSENTIAL, expected_msg); +} + +#[test] +fn public_avif_ipma_bad_version() { + let expected_msg = "The ipma version 0 should be used unless 32-bit \ + item_ID values are needed \ + per ISOBMFF (ISO 14496-12:2020 § 8.11.14.1"; + assert_avif_should(IMAGE_AVIF_IPMA_BAD_VERSION, expected_msg); +} + +#[test] +fn public_avif_ipma_bad_flags() { + let expected_msg = "Unless there are more than 127 properties in the \ + ItemPropertyContainerBox, flags should be equal to 0 \ + per ISOBMFF (ISO 14496-12:2020 § 8.11.14.1"; + assert_avif_should(IMAGE_AVIF_IPMA_BAD_FLAGS, expected_msg); +} + +#[test] +fn public_avif_ipma_duplicate_version_and_flags() { + let expected_msg = "There shall be at most one ItemPropertyAssociationbox \ + with a given pair of values of version and flags \ + per ISOBMFF (ISO 14496-12:2020 § 8.11.14.1"; + assert_avif_shall(IMAGE_AVIF_IPMA_DUPLICATE_VERSION_AND_FLAGS, expected_msg); +} + +#[test] +// TODO: convert this to a `assert_avif_shall` test to cover all `ParseStrictness` modes +// that would require crafting an input that validly uses multiple ipma boxes, +// which is kind of annoying to make pass the "should" requirements on flags and version +// as well as the "shall" requirement on duplicate version and flags +fn public_avif_ipma_duplicate_item_id() { + let expected_msg = "There shall be at most one occurrence of a given item_ID, \ + in the set of ItemPropertyAssociationBox boxes \ + per ISOBMFF (ISO 14496-12:2020) § 8.11.14.1"; + let input = &mut File::open(IMAGE_AVIF_IPMA_DUPLICATE_ITEM_ID).expect("Unknown file"); + assert_invalid_data( + mp4::read_avif(input, ParseStrictness::Permissive), + expected_msg, + ) +} + +#[test] +fn public_avif_ipma_invalid_property_index() { + let expected_msg = "Invalid property index in ipma"; + assert_avif_shall(IMAGE_AVIF_IPMA_INVALID_PROPERTY_INDEX, expected_msg); +} + +#[test] +fn public_avif_hdlr_first_in_meta() { + let expected_msg = "The HandlerBox shall be the first contained box within \ + the MetaBox \ + per MIAF (ISO 23000-22:2019) § 7.2.1.5"; + assert_avif_shall(IMAGE_AVIF_NO_HDLR, expected_msg); + assert_avif_shall(IMAGE_AVIF_HDLR_NOT_FIRST, expected_msg); +} + +#[test] +fn public_avif_hdlr_is_pict() { + let expected_msg = "The HandlerBox handler_type must be 'pict' \ + per MIAF (ISO 23000-22:2019) § 7.2.1.5"; + assert_avif_shall(IMAGE_AVIF_HDLR_NOT_PICT, expected_msg); +} + +#[test] +fn public_avif_hdlr_nonzero_reserved() { + let expected_msg = "The HandlerBox 'reserved' fields shall be 0 \ + per ISOBMFF (ISO 14496-12:2020) § 8.4.3.2"; + // This is a "should" despite the spec indicating a (somewhat ambiguous) + // requirement that this field is set to zero. + assert_avif_should(IMAGE_AVIF_HDLR_NONZERO_RESERVED, expected_msg); +} + +#[test] +fn public_avif_no_mif1() { + let expected_msg = "The FileTypeBox should contain 'mif1' in the compatible_brands list \ + per MIAF (ISO 23000-22:2019) § 7.2.1.2"; + assert_avif_should(IMAGE_AVIF_NO_MIF1, expected_msg); +} + +#[test] +fn public_avif_pixi_present_for_displayable_images() { + let expected_msg = "The pixel information property shall be associated with every image \ + that is displayable (not hidden) \ + per MIAF (ISO/IEC 23000-22:2019) specification § 7.3.6.6"; + let pixi_test = if cfg!(feature = "missing-pixi-permitted") { + assert_avif_should + } else { + assert_avif_shall + }; + + pixi_test(IMAGE_AVIF_NO_PIXI, expected_msg); + pixi_test(IMAGE_AVIF_NO_ALPHA_PIXI, expected_msg); +} + +#[test] +fn public_avif_av1c_present_for_av01() { + let expected_msg = "One AV1 Item Configuration Property (av1C) \ + is mandatory for an image item of type 'av01' \ + per AVIF specification § 2.2.1"; + assert_avif_shall(IMAGE_AVIF_NO_AV1C, expected_msg); + assert_avif_shall(IMAGE_AVIF_NO_ALPHA_AV1C, expected_msg); +} + +#[test] +fn public_avif_ispe_present() { + let expected_msg = "Missing 'ispe' property for primary item, required \ + per HEIF (ISO/IEC 23008-12:2017) § 6.5.3.1"; + assert_avif_shall(IMAGE_AVIF_NO_ISPE, expected_msg); +} + +fn assert_unsupported(result: mp4::Result, expected_status: mp4::Status) { + match result { + Err(Error::UnsupportedDetail(status, _msg)) if status == expected_status => {} + Err(Error::UnsupportedDetail(status, _msg)) if status != expected_status => { + panic!( + "Error message mismatch\nExpected: {:?}\nFound: {:?}", + expected_status, status + ); + } + r => panic!( + "Expected Err({:?}), found {:?}", + Error::from(expected_status), + r + ), + } +} + +#[test] +fn public_avif_a1lx() { + let input = &mut File::open(AVIF_A1LX).expect("Unknown file"); + assert_unsupported( + mp4::read_avif(input, ParseStrictness::Normal), + mp4::Status::UnsupportedA1lx, + ); +} + +#[test] +fn public_avif_a1op() { + let input = &mut File::open(AVIF_A1OP).expect("Unknown file"); + assert_unsupported( + mp4::read_avif(input, ParseStrictness::Normal), + mp4::Status::UnsupportedA1op, + ); +} + +#[test] +fn public_avif_clap() { + let input = &mut File::open(AVIF_CLAP).expect("Unknown file"); + assert_unsupported( + mp4::read_avif(input, ParseStrictness::Normal), + mp4::Status::UnsupportedClap, + ); +} + +#[test] +fn public_avif_grid() { + let input = &mut File::open(AVIF_GRID).expect("Unknown file"); + assert_unsupported( + mp4::read_avif(input, ParseStrictness::Normal), + mp4::Status::UnsupportedGrid, + ); +} + +#[test] +fn public_avif_lsel() { + let input = &mut File::open(AVIF_LSEL).expect("Unknown file"); + assert_unsupported( + mp4::read_avif(input, ParseStrictness::Normal), + mp4::Status::UnsupportedLsel, + ); +} + +#[test] +fn public_avif_read_samples() { + public_avif_read_samples_impl(ParseStrictness::Normal); +} + +#[test] +#[ignore] // See https://github.com/AOMediaCodec/av1-avif/issues/146 +fn public_avif_read_samples_strict() { + public_avif_read_samples_impl(ParseStrictness::Strict); +} + +fn to_canonical_paths(strs: &[&str]) -> Vec { + strs.iter() + .map(std::fs::canonicalize) + .map(Result::unwrap) + .collect() +} + +fn public_avif_read_samples_impl(strictness: ParseStrictness) { + let corrupt_images = to_canonical_paths(AV1_AVIF_CORRUPT_IMAGES); + let unsupported_images = to_canonical_paths(AVIF_UNSUPPORTED_IMAGES); + let legal_no_pixi_images = if cfg!(feature = "missing-pixi-permitted") { + to_canonical_paths(AVIF_NO_PIXI_IMAGES) + } else { + vec![] + }; + for dir in AVIF_TEST_DIRS { + for entry in walkdir::WalkDir::new(dir) { + let entry = entry.expect("AVIF entry"); + let path = entry.path(); + if !path.is_file() || path.extension().unwrap_or_default() != "avif" { + eprintln!("Skipping {:?}", path); + continue; // Skip directories, ReadMe.txt, etc. + } + let corrupt = (path.canonicalize().unwrap().parent().unwrap() + == std::fs::canonicalize(AVIF_CORRUPT_IMAGES_DIR).unwrap() + || corrupt_images.contains(&path.canonicalize().unwrap())) + && !legal_no_pixi_images.contains(&path.canonicalize().unwrap()); + + let unsupported = unsupported_images.contains(&path.canonicalize().unwrap()); + println!( + "parsing {}{}{:?}", + if corrupt { "(corrupt) " } else { "" }, + if unsupported { "(unsupported) " } else { "" }, + path, + ); + let input = &mut File::open(path).expect("Unknow file"); + match mp4::read_avif(input, strictness) { + Ok(_) if unsupported || corrupt => panic!("Expected error parsing {:?}", path), + Ok(_) => eprintln!("Successfully parsed {:?}", path), + Err(e @ Error::Unsupported(_)) | Err(e @ Error::UnsupportedDetail(..)) + if unsupported => + { + eprintln!( + "Expected error parsing unsupported input {:?}: {:?}", + path, e + ) + } + Err(e) if corrupt => { + eprintln!("Expected error parsing corrupt input {:?}: {:?}", path, e) + } + Err(e) => panic!("Unexected error parsing {:?}: {:?}", path, e), + } + } + } +} + +#[test] +fn public_video_h263() { + let mut fd = File::open(VIDEO_H263_3GP).expect("Unknown file"); + let mut buf = Vec::new(); + fd.read_to_end(&mut buf).expect("File error"); + + let mut c = Cursor::new(&buf); + let context = mp4::read_mp4(&mut c).expect("read_mp4 failed"); + for track in context.tracks { + let stsd = track.stsd.expect("expected an stsd"); + let v = match stsd.descriptions.first().expect("expected a SampleEntry") { + mp4::SampleEntry::Video(ref v) => v, + _ => panic!("expected a VideoSampleEntry"), + }; + assert_eq!(v.codec_type, mp4::CodecType::H263); + assert_eq!(v.width, 176); + assert_eq!(v.height, 144); + let _codec_specific = match v.codec_specific { + mp4::VideoCodecSpecific::H263Config(_) => true, + _ => { + panic!("expected a H263Config",); + } + }; + } +} + +#[test] +#[cfg(feature = "3gpp")] +fn public_audio_amrnb() { + let mut fd = File::open(AUDIO_AMRNB_3GP).expect("Unknown file"); + let mut buf = Vec::new(); + fd.read_to_end(&mut buf).expect("File error"); + + let mut c = Cursor::new(&buf); + let context = mp4::read_mp4(&mut c).expect("read_mp4 failed"); + for track in context.tracks { + let stsd = track.stsd.expect("expected an stsd"); + let a = match stsd.descriptions.first().expect("expected a SampleEntry") { + mp4::SampleEntry::Audio(ref v) => v, + _ => panic!("expected a AudioSampleEntry"), + }; + assert!(a.codec_type == mp4::CodecType::AMRNB); + let _codec_specific = match a.codec_specific { + mp4::AudioCodecSpecific::AMRSpecificBox(_) => true, + _ => { + panic!("expected a AMRSpecificBox",); + } + }; + } +} + +#[test] +#[cfg(feature = "3gpp")] +fn public_audio_amrwb() { + let mut fd = File::open(AUDIO_AMRWB_3GP).expect("Unknown file"); + let mut buf = Vec::new(); + fd.read_to_end(&mut buf).expect("File error"); + + let mut c = Cursor::new(&buf); + let context = mp4::read_mp4(&mut c).expect("read_mp4 failed"); + for track in context.tracks { + let stsd = track.stsd.expect("expected an stsd"); + let a = match stsd.descriptions.first().expect("expected a SampleEntry") { + mp4::SampleEntry::Audio(ref v) => v, + _ => panic!("expected a AudioSampleEntry"), + }; + assert!(a.codec_type == mp4::CodecType::AMRWB); + let _codec_specific = match a.codec_specific { + mp4::AudioCodecSpecific::AMRSpecificBox(_) => true, + _ => { + panic!("expected a AMRSpecificBox",); + } + }; + } +} + +#[test] +#[cfg(feature = "mp4v")] +fn public_video_mp4v() { + let mut fd = File::open(VIDEO_MP4V_MP4).expect("Unknown file"); + let mut buf = Vec::new(); + fd.read_to_end(&mut buf).expect("File error"); + + let mut c = Cursor::new(&buf); + let context = mp4::read_mp4(&mut c).expect("read_mp4 failed"); + for track in context.tracks { + let stsd = track.stsd.expect("expected an stsd"); + let v = match stsd.descriptions.first().expect("expected a SampleEntry") { + mp4::SampleEntry::Video(ref v) => v, + _ => panic!("expected a VideoSampleEntry"), + }; + assert_eq!(v.codec_type, mp4::CodecType::MP4V); + assert_eq!(v.width, 176); + assert_eq!(v.height, 144); + let _codec_specific = match v.codec_specific { + mp4::VideoCodecSpecific::ESDSConfig(_) => true, + _ => { + panic!("expected a ESDSConfig",); + } + }; + } +} diff --git a/tests/valid-alpha.avif b/tests/valid-alpha.avif new file mode 100644 index 0000000000000000000000000000000000000000..b192607495b269b18e60d43bb7d1ddd286dec3fb GIT binary patch literal 450 zcmYLF!AiqG5S^qU28pGJh!%x-7co7R+-iF8pdYcMi5tR#F|y{yoeV>j@7wU;vsI zR`%iCPl5Bo(Cd1H6RZt8d^5a>=|Yqy-_~6fzegdsKTLr&U??vkwkzaTcXn zE9snao%hcGAFJ)I?5E11zies){9uz+z%y=44wMjIa$nZr>_whP8&Su z;Lw#gbIeJFZ0k*;P5#m^4@t(ENt&*)xo#5laLKCf5;ffQnvL!+jji~bk>kFR>p9-L lkHVc^DSJxC-Uazzv(bb?h@(0ApHalK@Ek3V$nNRS{RcUUP09cO literal 0 HcmV?d00001 diff --git a/tests/valid-avif-colr-nclx-and-prof-and-rICC.avif b/tests/valid-avif-colr-nclx-and-prof-and-rICC.avif new file mode 100644 index 0000000000000000000000000000000000000000..b9fe1853ffc2049b17d181cf5eb5a43c9f3cfe43 GIT binary patch literal 1452 zcmeHGJ%|%Q6n?vR`5^=i8qXh8=J2OVLN?+VD@h_~Ai^0U=yj9HZZfz(!)A#|r+D@O z8|{wM&R#7n1WOMD1q%^>z`{mQ4sFh3@6B%FUSVNjd2g6`-+c4-z1=t80st|*k)Q)^ z0Gjj`H#Cb=<5ESd0K~3ax<}<1+I+8Lx*-G*oOx3`^}7W3tYD+ydX$?Ele_Z(3J$qK z>@NC)I8ShH8^NhXu% zGJ^K;nLp=gI?!sj2r>YBnPa*(Gl!t!h~ETI%aRhVgAGt~R0wkx<1E{An-{9&>a``3 z_z0=sjgP=#%KDQXRc7 zJN6r=&4h~$R!qdlZg@iCG2*O?FNOG_#N7}-mAD<^wIHU|tYOS2#Dw)|2fqy=`~JqN?{7Z9dhY?A+f&`R^&0khM>p8F y9o_tsZr~6XR;WY#K|x6<>h0xNJb@jYw>s3@1F1wA;$uR1DLsemMVzC?Yjq3Z1qE0D literal 0 HcmV?d00001 diff --git a/tests/valid-avif-colr-nclx-and-prof.avif b/tests/valid-avif-colr-nclx-and-prof.avif new file mode 100644 index 0000000000000000000000000000000000000000..683baa7f54f989b42c096a8eaceba96cfab1adac GIT binary patch literal 883 zcmb`FJ%|%Q6vzL&@sbdN292i&Ds%WzB_WA;#!8Y1dJy3Z5j5T2W;Yqwk72XKq*FZm zz%|+(r=6{+g@s^yAShUf_yHD{%Aw79tZ#M`bA_dEn0deX&)fHQ-z)%T`XiwYxCt`p zZEluqu9;V>r793B+lH^nGqS~A$MPZs1lRp3PyK$ueLLJJw4UIWtIORbK%pU5#MSU)4iX}qz2^XFqzs`m3l91(nVaOP=NkU`a6?)BB>jveJ;bre5;Qe!U9|<85B%dUb!PJyxB^7r*>yG#ruVcCeH$Tit9$hB9-h>d zG1;b}7ua&^dcMhOt-|@@h5R|G|5x*kVfgdZT`}@_x977eb<1GOj$X*IaFq#1-}K+FYCt b7VVDaA4DoqL43>@FQ?~8y+U);c%%LSslJ+f literal 0 HcmV?d00001 diff --git a/tests/valid-avif-colr-nclx-and-rICC.avif b/tests/valid-avif-colr-nclx-and-rICC.avif new file mode 100644 index 0000000000000000000000000000000000000000..eea6c788a75c3943e92339a3d98e72372df326fd GIT binary patch literal 883 zcmb`FO^DM#6vzKb-L{mXMeFKw;! z_M*3C_2yO7g9pLuf}r3*#1HV`QCak|F7Zv$+C6ymg_-x8|Gd06^AZ4>>5T;&aua0I z+uSVJoS9e4g)$H++J?vE9@%oQW4R#$g6rOlXMVrno*gU{non@6ugldXK%qy@h_uKr z(KyYyV@i=sy2G?I)ai;1^94fn2^X#)zs`l;Cn3!T!jLg!(}YL~KH^e{#N%;tDZvKx z&fonc8y2c}G*W;=df###-5NoqCwiMwm!#y@s#W3)6T;d@akk^Rjms77#?6)%`-DVr z;^R2R^noa^uGVF)nU_4bU+;p#c$<~EUfo}5O;qRc#V2Y!+Cr2z bmhBGn4k8gNAv)1CUQ8~MdWFWg{zm-+jlG%$ literal 0 HcmV?d00001 diff --git a/tests/valid-avif-colr-nclx.avif b/tests/valid-avif-colr-nclx.avif new file mode 100644 index 0000000000000000000000000000000000000000..3aa4b8f5bbdcfa8a9f5af7e0b26dda26185a0cf5 GIT binary patch literal 314 zcmXv|&1%9x5S~P|L97&8MK2{e^xpL3RqUaM_6a;KYZeExW{68rytLQ{=(G7Y-ujJg zzGde7V_=CWb>SeUVMl`IRCey0=wXZ*93pdrQ4fL{k=Tv;*+VZWN!syU>|mn=qpO&768aY058y M3DP&r;r$%{0!L^tK>z>% literal 0 HcmV?d00001 diff --git a/tests/valid-avif-colr-prof-and-rICC.avif b/tests/valid-avif-colr-prof-and-rICC.avif new file mode 100644 index 0000000000000000000000000000000000000000..a4609aa93e4da55b2d71ffae7627f7559248a719 GIT binary patch literal 1432 zcmeH`O^DM#6vtn(+Yd_7B6W2^Wfnh9ZD}g5^(1WtEmT-TMcvypP1=Ec3`wZllZfj* z;6-oC>dm|E!Gqv+K~NAB@dG?~R2IFgOMH{GW)B`bc-$9e-f#Z%GB0^~0RUq7V@^lR z05st(W+)b;#^r|607zZ4bf1bnw1r{cbYleIO!KEa_4_#Ut!N?Pe2kfnCRP^!B)r57 zsaxphahzn#HiU>K+(BIG@?=FvRRwYOE#odnzrncXpipEU*F_B4)_kUCCa%ui>hV&u6nUauHRTDnU9c*9`7g|p_<1V zOG_Pb=hO>M?B|;(3%o9ivtHa^Tn=RW@%hi+JL>l0S`znH%Jp?ml;;-?TjMTPQ!mCp$MznZVFM4i%d_}FEGp-&oEEuFM&tx_Vjs)8v+?c=0dSK==>x0z_p zIzasge&Ha|dJh1e-UT@PF40b`0qkD|c=D1{mqzE!;4Ig_0DS!haHxRiAQzu`#_3wL zFXfxqlpn%y>l8rd9>7izhTH35xbqP6y$5)1Py5Dgx3JE8eS>w|>zjY-8<=O@iu(|M lP>|A+d}lS4#sV%_ed-^8T&51`fDl&8&!Kt=$GG!K{tdUe0R8{~ literal 0 HcmV?d00001 diff --git a/tests/valid-avif-colr-prof.avif b/tests/valid-avif-colr-prof.avif new file mode 100644 index 0000000000000000000000000000000000000000..2ee8fa86b3348e810075f1f001b35f9d9dad7093 GIT binary patch literal 863 zcmb`FJ%|%Q6vzL&F(!ndLE?FW9&`9nB_WA;#!8ZidJy3Z5%jvrW;Yqg?hczJUOL6I z57=mToObqVVIkNa2nrS=et?Cga%giN>zm!gTw&>3X5Mf9^Y*=&w+nz-{z&qnut27~ zEv%9)xOKH!ssgbE+w{5GBU|is`d);96oxD=Q84 z=F}^m*{^qD6}-)>T(2H1t&Vl)$%QXJ8qJ-hjal43Db+Ctsy)AHIEM77n_8dv#4TSc z9usFhIu!9^#odUXD{e=;6~?rhb&B~+%#0rI(A@};Ywyv!dw5b;#bleNQDDoh>-i?D zwF>8o=ksTk{$I^E*TO%g?#huTx;>v&saqync8o%fm5U`Rsd^t3-M$umxw);G=57L& zpY(;tGp+p)cy%=^uM-o7{Ub^$QU8%aJC7RZ#h zg;lTxx2~28WgxO-n;uttWXs)l&kYff!tiE1^9Q8x>|mkMyd-*kL#-|X8a--8=P2f@s8sdHwLo2x>{Fn z&b;Ee{dyNv!P~6L_3FXO+E{m%-q=~0%;Ww^iMBaV?fFf^F{De~)Oy4xZh2Di zm^kgyp@<(V?u7hYaVzAlAg0-%`)HqunbGBKx*H)ftvz~Y4^Qi=m~_K5a;(<8o^7y7 zGj}e3K6_T_|J6KmJ@`|qeK~SPr|YpYb<1Qm$H-+^F<+pPs`p{h?Q7weTiBXw{Y{|s zlfLkHuC*Ql&+Y+>@8{ad32<-&c=}p$mk0SGH0!0Wz_;(fkrd@2PQMsfXNtkTmaG8L zkG{Ws8i?HocE`TIv+4W0kEq@U;H5)xi$dCAK58_ysHWfDh?2f+x4Cx+iC78y$Bc<$ Na)HzQ;cjM$L>)^2a5S^K6D$!KvOgIV;u+O&d;5~eW`8rkK zU#f{Hap5esWJiFqXP0`m?st(c5Z!$dtaiY7lWz|%NhF+wN>|(w4*qEj_K3IpOx=>m z;OQpQ1`jb4=AF|JQ0l=J(c9T_nyOs090a^#$@btcK1!hl@cxjx!Y9_C7RNEDEB2^2 uRo}8@`h7`|5Di$oeJbru-9*