Import mp4parse 0.12.0 upstream/0.12.0
authorDongHun Kwak <dh0128.kwak@samsung.com>
Thu, 6 Apr 2023 00:10:41 +0000 (09:10 +0900)
committerDongHun Kwak <dh0128.kwak@samsung.com>
Thu, 6 Apr 2023 00:10:41 +0000 (09:10 +0900)
54 files changed:
.cargo_vcs_info.json [new file with mode: 0644]
Cargo.toml [new file with mode: 0644]
Cargo.toml.orig [new file with mode: 0644]
LICENSE [new file with mode: 0644]
README.md [new file with mode: 0644]
benches/avif_benchmark.rs [new file with mode: 0644]
src/boxes.rs [new file with mode: 0644]
src/lib.rs [new file with mode: 0644]
src/macros.rs [new file with mode: 0644]
src/tests.rs [new file with mode: 0644]
src/unstable.rs [new file with mode: 0644]
tests/1x1-black-alpha-50pct-premultiplied.avif [new file with mode: 0644]
tests/amr_nb_1f.3gp [new file with mode: 0644]
tests/amr_wb_1f.3gp [new file with mode: 0644]
tests/av1C-missing-essential.avif [new file with mode: 0644]
tests/bad-ipma-flags.avif [new file with mode: 0644]
tests/bad-ipma-version.avif [new file with mode: 0644]
tests/bbb_sunflower_QCIF_30fps_h263_noaudio_1f.3gp [new file with mode: 0644]
tests/clap-basic-1_3x3-to-1x1.avif [new file with mode: 0644]
tests/clusterfuzz-testcase-minimized-mp4-6093954524250112 [new file with mode: 0644]
tests/corrupt/bug-1655846.avif [new file with mode: 0644]
tests/corrupt/bug-1661347.avif [new file with mode: 0644]
tests/corrupt/hdlr-not-first.avif [new file with mode: 0644]
tests/corrupt/hdlr-not-pict.avif [new file with mode: 0644]
tests/corrupt/imir-before-clap.avif [new file with mode: 0644]
tests/corrupt/invalid-avif-colr-multiple-nclx.avif [new file with mode: 0644]
tests/corrupt/invalid-avif-colr-multiple-prof.avif [new file with mode: 0644]
tests/corrupt/invalid-avif-colr-multiple-rICC.avif [new file with mode: 0644]
tests/corrupt/invalid-avif-colr-multiple.zip [new file with mode: 0644]
tests/corrupt/ipma-duplicate-item_id.avif [new file with mode: 0644]
tests/corrupt/ipma-duplicate-version-and-flags.avif [new file with mode: 0644]
tests/corrupt/ipma-invalid-property-index.avif [new file with mode: 0644]
tests/corrupt/no-alpha-av1C.avif [new file with mode: 0644]
tests/corrupt/no-av1C.avif [new file with mode: 0644]
tests/corrupt/no-hdlr.avif [new file with mode: 0644]
tests/corrupt/no-ispe.avif [new file with mode: 0644]
tests/corrupt/no-pixi-for-alpha.avif [new file with mode: 0644]
tests/corrupt/no-pixi.avif [new file with mode: 0644]
tests/hdlr-nonzero-reserved.avif [new file with mode: 0644]
tests/imir-missing-essential.avif [new file with mode: 0644]
tests/irot-missing-essential.avif [new file with mode: 0644]
tests/multiple-extents.avif [new file with mode: 0644]
tests/no-mif1.avif [new file with mode: 0644]
tests/overflow.rs [new file with mode: 0644]
tests/public.rs [new file with mode: 0644]
tests/valid-alpha.avif [new file with mode: 0644]
tests/valid-avif-colr-nclx-and-prof-and-rICC.avif [new file with mode: 0644]
tests/valid-avif-colr-nclx-and-prof.avif [new file with mode: 0644]
tests/valid-avif-colr-nclx-and-rICC.avif [new file with mode: 0644]
tests/valid-avif-colr-nclx.avif [new file with mode: 0644]
tests/valid-avif-colr-prof-and-rICC.avif [new file with mode: 0644]
tests/valid-avif-colr-prof.avif [new file with mode: 0644]
tests/valid-avif-colr-rICC.avif [new file with mode: 0644]
tests/valid.avif [new file with mode: 0644]

diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json
new file mode 100644 (file)
index 0000000..0d7f400
--- /dev/null
@@ -0,0 +1,5 @@
+{
+  "git": {
+    "sha1": "e133798fb00969cad0561f0e92291d2d02803497"
+  }
+}
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644 (file)
index 0000000..e369c2b
--- /dev/null
@@ -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 <giles@mozilla.com>", "Matthew Gregan <kinetik@flim.org>", "Alfredo Yang <ayang@mozilla.com>", "Jon Bauman <jbauman@mozilla.com>", "Bryce Seager van Dyk <bvandyk@mozilla.com>"]
+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 (file)
index 0000000..1639edd
--- /dev/null
@@ -0,0 +1,55 @@
+[package]
+name = "mp4parse"
+version = "0.12.0"
+authors = [
+  "Ralph Giles <giles@mozilla.com>",
+  "Matthew Gregan <kinetik@flim.org>",
+  "Alfredo Yang <ayang@mozilla.com>",
+  "Jon Bauman <jbauman@mozilla.com>",
+  "Bryce Seager van Dyk <bvandyk@mozilla.com>",
+]
+
+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 (file)
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 (file)
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 (file)
index 0000000..0d8a48b
--- /dev/null
@@ -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 (file)
index 0000000..d80798c
--- /dev/null
@@ -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<u32> for BoxType {
+            fn from(t: u32) -> BoxType {
+                use self::BoxType::*;
+                match t {
+                    $($(#[$attr])* $boxtype => $boxenum),*,
+                    _ => UnknownBox(t),
+                }
+            }
+        }
+
+        impl From<BoxType> 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<u32> for FourCC {
+    fn from(number: u32) -> FourCC {
+        FourCC {
+            value: number.to_be_bytes(),
+        }
+    }
+}
+
+impl From<BoxType> 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 (file)
index 0000000..464944f
--- /dev/null
@@ -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<usize> is used, it may panic.
+impl ToU64 for usize {
+    fn to_u64(self) -> u64 {
+        static_assertions::const_assert!(
+            std::mem::size_of::<usize>() <= std::mem::size_of::<u64>()
+        );
+        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<usize> 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::<usize>()
+                );
+                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<usize> {
+        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<T> = fallible_collections::TryVec<T>;
+pub type TryString = fallible_collections::TryVec<u8>;
+pub type TryHashMap<K, V> = fallible_collections::TryHashMap<K, V>;
+pub type TryBox<T> = fallible_collections::TryBox<T>;
+
+// 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<Status> 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<Error> 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<Result<(), Status>> for Status {
+    fn from(result: Result<(), Status>) -> Self {
+        match result {
+            Ok(()) => Status::Ok,
+            Err(Status::Ok) => unreachable!(),
+            Err(e) => e,
+        }
+    }
+}
+
+impl From<fallible_collections::TryReserveError> 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<Status> for Error`](enum.Error.html#impl-From<Status>)
+    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<bitreader::BitReaderError> for Error {
+    fn from(_: bitreader::BitReaderError) -> Error {
+        Error::InvalidData("invalid data")
+    }
+}
+
+impl From<std::io::Error> for Error {
+    fn from(err: std::io::Error) -> Error {
+        match err.kind() {
+            std::io::ErrorKind::UnexpectedEof => Error::UnexpectedEOF,
+            _ => Error::Io(err),
+        }
+    }
+}
+
+impl From<std::string::FromUtf8Error> for Error {
+    fn from(_: std::string::FromUtf8Error) -> Error {
+        Error::InvalidData("invalid utf8")
+    }
+}
+
+impl From<std::str::Utf8Error> for Error {
+    fn from(_: std::str::Utf8Error) -> Error {
+        Error::InvalidData("invalid utf8")
+    }
+}
+
+impl From<std::num::TryFromIntError> for Error {
+    fn from(_: std::num::TryFromIntError) -> Error {
+        Error::Unsupported("integer conversion failed")
+    }
+}
+
+impl From<Error> 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<TryReserveError> for Error {
+    fn from(_: TryReserveError) -> Error {
+        Error::OutOfMemory
+    }
+}
+
+/// Result shorthand using our Error enum.
+pub type Result<T, E = Error> = std::result::Result<T, E>;
+
+/// 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<FourCC>,
+}
+
+/// 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<Edit>,
+}
+
+#[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<u64>,
+}
+
+// Sync sample box 'stss'
+#[derive(Debug)]
+pub struct SyncSampleBox {
+    pub samples: TryVec<u32>,
+}
+
+// Sample to chunk box 'stsc'
+#[derive(Debug)]
+pub struct SampleToChunkBox {
+    pub samples: TryVec<SampleToChunk>,
+}
+
+#[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<u32>,
+}
+
+// Time to sample box 'stts'
+#[derive(Debug)]
+pub struct TimeToSampleBox {
+    pub samples: TryVec<Sample>,
+}
+
+#[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<TimeOffset>,
+}
+
+// Handler reference box 'hdlr'
+#[derive(Debug)]
+struct HandlerBox {
+    handler_type: FourCC,
+}
+
+// Sample description box 'stsd'
+#[derive(Debug)]
+pub struct SampleDescriptionBox {
+    pub descriptions: TryVec<SampleEntry>,
+}
+
+#[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<u16>,
+    pub extended_audio_object_type: Option<u16>,
+    pub audio_sample_rate: Option<u32>,
+    pub audio_channel_count: Option<u16>,
+    #[cfg(feature = "mp4v")]
+    pub video_codec: CodecType,
+    pub codec_esds: TryVec<u8>,
+    pub decoder_specific_data: TryVec<u8>, // 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<u8>),
+}
+
+#[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<ProtectionSchemeInfoBox>,
+}
+
+#[derive(Debug)]
+pub enum VideoCodecSpecific {
+    AVCConfig(TryVec<u8>),
+    VPxConfig(VPxConfigBox),
+    AV1Config(AV1ConfigBox),
+    ESDSConfig(TryVec<u8>),
+    H263Config(TryVec<u8>),
+}
+
+#[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<ProtectionSchemeInfoBox>,
+}
+
+/// 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<u8>,
+    /// 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<u8>,
+}
+
+/// 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<u8>,
+}
+
+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<u8>,
+}
+
+/// 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<FLACMetadataBlock>,
+}
+
+#[derive(Debug)]
+struct ChannelMappingTable {
+    stream_count: u8,
+    coupled_count: u8,
+    channel_mapping: TryVec<u8>,
+}
+
+/// 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<ChannelMappingTable>,
+}
+
+/// 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<u8>,
+}
+
+#[derive(Debug)]
+pub struct MovieExtendsBox {
+    pub fragment_duration: Option<MediaScaledTime>,
+}
+
+pub type ByteData = TryVec<u8>;
+
+#[derive(Debug, Default)]
+pub struct ProtectionSystemSpecificHeaderBox {
+    pub system_id: ByteData,
+    pub kid: TryVec<ByteData>,
+    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<u8>,
+    // Members for pattern encryption schemes
+    pub crypt_byte_block_count: Option<u8>,
+    pub skip_byte_block_count: Option<u8>,
+    pub constant_iv: Option<TryVec<u8>>,
+    // End pattern encryption scheme members
+}
+
+#[derive(Debug, Default)]
+pub struct ProtectionSchemeInfoBox {
+    pub original_format: FourCC,
+    pub scheme_type: Option<SchemeTypeBox>,
+    pub tenc: Option<TrackEncryptionBox>,
+}
+
+/// Represents a userdata box 'udta'.
+/// Currently, only the metadata atom 'meta'
+/// is parsed.
+#[derive(Debug, Default)]
+pub struct UserdataBox {
+    pub meta: Option<MetadataBox>,
+}
+
+/// 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<TryString>,
+    /// The artist name '©art' or '©ART'
+    pub artist: Option<TryString>,
+    /// The album artist 'aART'
+    pub album_artist: Option<TryString>,
+    /// Track comments '©cmt'
+    pub comment: Option<TryString>,
+    /// 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<TryString>,
+    /// The track title '©nam'
+    pub title: Option<TryString>,
+    /// The track genre '©gen' or 'gnre'.
+    pub genre: Option<Genre>,
+    /// The track number 'trkn'.
+    pub track_number: Option<u8>,
+    /// The disc number 'disk'
+    pub disc_number: Option<u8>,
+    /// The total number of tracks on the disc,
+    /// stored in 'trkn'
+    pub total_tracks: Option<u8>,
+    /// The total number of discs in the album,
+    /// stored in 'disk'
+    pub total_discs: Option<u8>,
+    /// The composer of the track '©wrt'
+    pub composer: Option<TryString>,
+    /// The encoder used to create this track '©too'
+    pub encoder: Option<TryString>,
+    /// The encoded-by settingo this track '©enc'
+    pub encoded_by: Option<TryString>,
+    /// The tempo or BPM of the track 'tmpo'
+    pub beats_per_minute: Option<u8>,
+    /// Copyright information of the track 'cprt'
+    pub copyright: Option<TryString>,
+    /// Whether or not this track is part of a compilation 'cpil'
+    pub compilation: Option<bool>,
+    /// The advisory rating of this track 'rtng'
+    pub advisory: Option<AdvisoryRating>,
+    /// 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<TryString>,
+    /// The grouping this track belongs to '©grp'
+    pub grouping: Option<TryString>,
+    /// The media type of this track 'stik'
+    pub media_type: Option<MediaType>, // stik
+    /// Whether or not this track is a podcast 'pcst'
+    pub podcast: Option<bool>,
+    /// The category of ths track 'catg'
+    pub category: Option<TryString>,
+    /// The podcast keyword 'keyw'
+    pub keyword: Option<TryString>,
+    /// The podcast url 'purl'
+    pub podcast_url: Option<TryString>,
+    /// The podcast episode GUID 'egid'
+    pub podcast_guid: Option<TryString>,
+    /// The description of the track 'desc'
+    pub description: Option<TryString>,
+    /// 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<TryString>,
+    /// The lyrics of the track '©lyr'.
+    ///
+    /// Unlike other string fields, the lyrics field
+    /// can be longer than 256 characters.
+    pub lyrics: Option<TryString>,
+    /// The name of the TV network this track aired on 'tvnn'.
+    pub tv_network_name: Option<TryString>,
+    /// The name of the TV Show for this track 'tvsh'.
+    pub tv_show_name: Option<TryString>,
+    /// The name of the TV Episode for this track 'tven'.
+    pub tv_episode_name: Option<TryString>,
+    /// The number of the TV Episode for this track 'tves'.
+    pub tv_episode_number: Option<u8>,
+    /// The season of the TV Episode of this track 'tvsn'.
+    pub tv_season: Option<u8>,
+    /// The date this track was purchased 'purd'.
+    pub purchase_date: Option<TryString>,
+    /// Whether or not this track supports gapless playback 'pgap'
+    pub gapless_playback: Option<bool>,
+    /// 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<TryVec<TryVec<u8>>>,
+    /// The owner of the track 'ownr'
+    pub owner: Option<TryString>,
+    /// Whether or not this track is HD Video 'hdvd'
+    pub hd_video: Option<bool>,
+    /// The name of the track to sort by 'sonm'
+    pub sort_name: Option<TryString>,
+    /// The name of the album to sort by 'soal'
+    pub sort_album: Option<TryString>,
+    /// The name of the artist to sort by 'soar'
+    pub sort_artist: Option<TryString>,
+    /// The name of the album artist to sort by 'soaa'
+    pub sort_album_artist: Option<TryString>,
+    /// The name of the composer to sort by 'soco'
+    pub sort_composer: Option<TryString>,
+    /// Metadata
+    #[cfg(feature = "meta-xml")]
+    pub xml: Option<XmlBox>,
+}
+
+/// 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<u8>),
+}
+
+/// Internal data structures.
+#[derive(Debug, Default)]
+pub struct MediaContext {
+    pub timescale: Option<MediaTimeScale>,
+    /// Tracks found in the file.
+    pub tracks: TryVec<Track>,
+    pub mvex: Option<MovieExtendsBox>,
+    pub psshs: TryVec<ProtectionSystemSpecificHeaderBox>,
+    pub userdata: Option<Result<UserdataBox>>,
+    #[cfg(feature = "meta-xml")]
+    pub metadata: Option<Result<MetadataBox>>,
+}
+
+/// 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<u8>),
+}
+
+#[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 <https://aomediacodec.github.io/av1-avif/#image-item>
+    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<MediaDataBox>,
+    /// 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<AvifItem>,
+    /// 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<ImageRotation> {
+        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<SingleItemTypeReferenceBox>,
+    item_properties: ItemPropertiesBox,
+    primary_item_id: ItemId,
+    iloc_items: TryHashMap<ItemId, ItemLocationBoxItem>,
+}
+
+/// 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<u8>,
+}
+
+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<usize> {
+        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<u8>) -> 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<ItemId> {
+        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<u8> for IlocFieldSize {
+    type Error = Error;
+
+    fn try_from(value: u8) -> Result<Self> {
+        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<u8> for IlocVersion {
+    type Error = Error;
+
+    fn try_from(value: u8) -> Result<Self> {
+        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<Extent>,
+}
+
+/// See ISOBMFF (ISO 14496-12:2020) § 8.11.3
+///
+/// Note: per MIAF (ISO 23000-22:2019) § 7.2.1.7:<br />
+/// > MIAF image items are constrained as follows:<br />
+/// > — `construction_method` shall be equal to 0 for MIAF image items that are coded image items.<br />
+/// > — `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<T: Num>(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<T>(pub T, pub usize);
+
+impl<T> std::ops::Add for TrackScaledTime<T>
+where
+    T: num_traits::CheckedAdd,
+{
+    type Output = Option<Self>;
+
+    fn add(self, other: TrackScaledTime<T>) -> 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<MediaScaledTime>,
+    pub media_time: Option<TrackScaledTime<u64>>,
+    pub timescale: Option<TrackTimeScale<u64>>,
+    pub duration: Option<TrackScaledTime<u64>>,
+    pub track_id: Option<u32>,
+    pub tkhd: Option<TrackHeaderBox>, // TODO(kinetik): find a nicer way to export this.
+    pub stsd: Option<SampleDescriptionBox>,
+    pub stts: Option<TimeToSampleBox>,
+    pub stsc: Option<SampleToChunkBox>,
+    pub stsz: Option<SampleSizeBox>,
+    pub stco: Option<ChunkOffsetBox>, // It is for stco or co64.
+    pub stss: Option<SyncSampleBox>,
+    pub ctts: Option<CompositionOffsetBox>,
+}
+
+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<T> {
+        BoxIter { src }
+    }
+
+    fn next_box(&mut self) -> Result<Option<BMFFBox<T>>> {
+        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<usize> {
+        self.content.read(buf)
+    }
+}
+
+impl<'a, T: Read> TryRead for BMFFBox<'a, T> {
+    fn try_read_to_end(&mut self, buf: &mut TryVec<u8>) -> std::io::Result<usize> {
+        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<BMFFBox<'a, T>> {
+        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<T: ReadBytesExt>(src: &mut T) -> Result<BoxHeader> {
+    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<T: ReadBytesExt>(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<T: ReadBytesExt>(src: &mut T) -> Result<u8> {
+    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<T: Read>(src: &mut BMFFBox<T>) -> 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<T: Read>(src: &mut BMFFBox<T>) -> 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<T: Read>(f: &mut T, strictness: ParseStrictness) -> Result<AvifContext> {
+    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<T: Read + Offset>(
+    src: &mut BMFFBox<T>,
+    strictness: ParseStrictness,
+) -> Result<AvifMeta> {
+    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<T: Read>(src: &mut BMFFBox<T>) -> Result<ItemId> {
+    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<T: Read>(
+    src: &mut BMFFBox<T>,
+    strictness: ParseStrictness,
+) -> Result<TryVec<ItemInfoEntry>> {
+    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<T: Read>(src: &mut BMFFBox<T>, strictness: ParseStrictness) -> Result<ItemInfoEntry> {
+    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<T: Read>(src: &mut BMFFBox<T>) -> Result<TryVec<SingleItemTypeReferenceBox>> {
+    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<T: Read>(
+    src: &mut BMFFBox<T>,
+    brand: FourCC,
+    strictness: ParseStrictness,
+) -> Result<ItemPropertiesBox> {
+    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::<ItemPropertyAssociationEntry>::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<FourCC, PropertyIndex> =
+                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<Association>,
+}
+
+/// 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<PropertyIndex, ItemProperty>,
+    /// `ItemPropertyAssociationBox association[]` in the spec
+    association_entries: TryVec<ItemPropertyAssociationEntry>,
+}
+
+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<Option<&ImageSpatialExtentsProperty>> {
+        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<Option<&ItemProperty>> {
+        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<TryVec<&ItemProperty>> {
+        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<U32MulU16> 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<usize> {
+    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<T: Read>(
+    src: &mut BMFFBox<T>,
+    strictness: ParseStrictness,
+    version: u8,
+    flags: u32,
+) -> Result<TryVec<ItemPropertyAssociationEntry>> {
+    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::<ItemPropertyAssociationEntry>::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<T: Read>(
+    src: &mut BMFFBox<T>,
+    strictness: ParseStrictness,
+) -> Result<TryHashMap<PropertyIndex, ItemProperty>> {
+    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<T: Read>(src: &mut BMFFBox<T>) -> Result<ImageSpatialExtentsProperty> {
+    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<u8>,
+}
+
+/// Parse pixel information
+/// See HEIF (ISO 23008-12:2017) § 6.5.6
+fn read_pixi<T: Read>(src: &mut BMFFBox<T>) -> Result<PixelInformation> {
+    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<u8>,
+}
+
+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<T: Read>(
+    src: &mut BMFFBox<T>,
+    strictness: ParseStrictness,
+) -> Result<ColourInformation> {
+    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<T: Read>(src: &mut BMFFBox<T>) -> Result<ImageRotation> {
+    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<br />
+/// 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<T: Read>(src: &mut BMFFBox<T>) -> Result<ImageMirror> {
+    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<T: Read>(src: &mut BMFFBox<T>) -> Result<AuxiliaryTypeProperty> {
+    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<u8>);
+    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<T: Read>(src: &mut BMFFBox<T>) -> Result<TryHashMap<ItemId, ItemLocationBoxItem>> {
+    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<IlocFieldSize> = 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<T: Read>(f: &mut T) -> Result<MediaContext> {
+    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<T: Read>(f: &mut BMFFBox<T>) -> Result<Option<MediaTimeScale>> {
+    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<T: Read>(f: &mut BMFFBox<T>, context: Option<MediaContext>) -> Result<MediaContext> {
+    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<T: Read>(src: &mut BMFFBox<T>) -> Result<ProtectionSystemSpecificHeaderBox> {
+    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::<ByteData>::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<T: Read>(src: &mut BMFFBox<T>) -> Result<MovieExtendsBox> {
+    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<T: Read>(src: &mut BMFFBox<T>) -> Result<MediaScaledTime> {
+    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<T: Read>(f: &mut BMFFBox<T>, 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<T: Read>(f: &mut BMFFBox<T>, 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::<u64>(
+                    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<T: Read>(
+    f: &mut BMFFBox<T>,
+    track: &mut Track,
+) -> Result<(
+    MediaHeaderBox,
+    Option<TrackScaledTime<u64>>,
+    Option<TrackTimeScale<u64>>,
+)> {
+    let mdhd = read_mdhd(f)?;
+    let duration = match mdhd.duration {
+        std::u64::MAX => None,
+        duration => Some(TrackScaledTime::<u64>(duration, track.id)),
+    };
+    if mdhd.timescale == 0 {
+        return Err(Error::InvalidData("zero timescale in mdhd"));
+    }
+    let timescale = Some(TrackTimeScale::<u64>(u64::from(mdhd.timescale), track.id));
+    Ok((mdhd, duration, timescale))
+}
+
+fn read_mdia<T: Read>(f: &mut BMFFBox<T>, 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<T: Read>(f: &mut BMFFBox<T>, 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<T: Read>(f: &mut BMFFBox<T>, 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<T: Read>(src: &mut BMFFBox<T>) -> Result<FileTypeBox> {
+    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<T: Read>(src: &mut BMFFBox<T>) -> Result<MovieHeaderBox> {
+    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<T: Read>(src: &mut BMFFBox<T>) -> Result<TrackHeaderBox> {
+    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<T: Read>(src: &mut BMFFBox<T>) -> Result<EditListBox> {
+    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<T: Read>(src: &mut BMFFBox<T>) -> Result<MediaHeaderBox> {
+    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<T: Read>(src: &mut BMFFBox<T>) -> Result<ChunkOffsetBox> {
+    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<T: Read>(src: &mut BMFFBox<T>) -> Result<ChunkOffsetBox> {
+    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<T: Read>(src: &mut BMFFBox<T>) -> Result<SyncSampleBox> {
+    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<T: Read>(src: &mut BMFFBox<T>) -> Result<SampleToChunkBox> {
+    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<T: Read>(src: &mut BMFFBox<T>) -> Result<CompositionOffsetBox> {
+    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<T: Read>(src: &mut BMFFBox<T>) -> Result<SampleSizeBox> {
+    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<T: Read>(src: &mut BMFFBox<T>) -> Result<TimeToSampleBox> {
+    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<T: Read>(src: &mut BMFFBox<T>) -> Result<VPxConfigBox> {
+    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<T: Read>(src: &mut BMFFBox<T>) -> Result<AV1ConfigBox> {
+    // 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<T: Read>(src: &mut BMFFBox<T>) -> Result<FLACMetadataBlock> {
+    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<u16> {
+    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<u32> = 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<u16> {
+    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<T: Read>(src: &mut BMFFBox<T>) -> Result<ES_Descriptor> {
+    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<T: Read>(src: &mut BMFFBox<T>) -> Result<FLACSpecificBox> {
+    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<T: Read>(src: &mut BMFFBox<T>) -> Result<OpusSpecificBox> {
+    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<W: byteorder::WriteBytesExt + std::io::Write>(
+    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::<byteorder::LittleEndian>(opus.pre_skip)?;
+    dst.write_u32::<byteorder::LittleEndian>(opus.input_sample_rate)?;
+    dst.write_i16::<byteorder::LittleEndian>(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<T: Read>(src: &mut BMFFBox<T>) -> Result<ALACSpecificBox> {
+    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.<br />
+/// See ISOBMFF (ISO 14496-12:2020) § 8.4.3<br />
+/// See [\[ISOBMFF\]: reserved (field = 0;) handling is ambiguous](https://github.com/MPEGGroup/FileFormat/issues/36)
+fn read_hdlr<T: Read>(src: &mut BMFFBox<T>, strictness: ParseStrictness) -> Result<HandlerBox> {
+    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<T: Read>(src: &mut BMFFBox<T>) -> Result<SampleEntry> {
+    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<T: Read>(src: &mut BMFFBox<T>) -> Result<ES_Descriptor> {
+    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<T: Read>(src: &mut BMFFBox<T>) -> Result<SampleEntry> {
+    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<T: Read>(src: &mut BMFFBox<T>, track: &mut Track) -> Result<SampleDescriptionBox> {
+    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<T: Read>(src: &mut BMFFBox<T>) -> Result<ProtectionSchemeInfoBox> {
+    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<T: Read>(src: &mut BMFFBox<T>) -> Result<Option<TrackEncryptionBox>> {
+    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<T: Read>(src: &mut BMFFBox<T>) -> Result<TrackEncryptionBox> {
+    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<T: Read>(src: &mut BMFFBox<T>) -> Result<SchemeTypeBox> {
+    // 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<T: Read>(src: &mut BMFFBox<T>) -> Result<UserdataBox> {
+    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<T: Read>(src: &mut BMFFBox<T>) -> Result<MetadataBox> {
+    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_<T: Read>(src: &mut BMFFBox<T>, 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<T: Read>(src: &mut BMFFBox<T>, 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<T: Read>(src: &mut BMFFBox<T>, 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<T: Read>(src: &mut BMFFBox<T>) -> Result<Option<bool>> {
+    Ok(read_ilst_u8_data(src)?.and_then(|d| Some(d.get(0)? == &1)))
+}
+
+fn read_ilst_string_data<T: Read>(src: &mut BMFFBox<T>) -> Result<Option<TryString>> {
+    read_ilst_u8_data(src)
+}
+
+fn read_ilst_u8_data<T: Read>(src: &mut BMFFBox<T>) -> Result<Option<TryVec<u8>>> {
+    // 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<T: Read>(src: &mut BMFFBox<T>) -> Result<TryVec<TryVec<u8>>> {
+    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<T: Read>(src: &mut BMFFBox<T>) -> Result<TryVec<u8>> {
+    // 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<T: Read>(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<T: Read>(src: &mut T, size: u64) -> Result<TryVec<u8>> {
+    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<T: ReadBytesExt>(src: &mut T) -> Result<i16> {
+    src.read_i16::<byteorder::BigEndian>().map_err(From::from)
+}
+
+fn be_i32<T: ReadBytesExt>(src: &mut T) -> Result<i32> {
+    src.read_i32::<byteorder::BigEndian>().map_err(From::from)
+}
+
+fn be_i64<T: ReadBytesExt>(src: &mut T) -> Result<i64> {
+    src.read_i64::<byteorder::BigEndian>().map_err(From::from)
+}
+
+fn be_u16<T: ReadBytesExt>(src: &mut T) -> Result<u16> {
+    src.read_u16::<byteorder::BigEndian>().map_err(From::from)
+}
+
+fn be_u24<T: ReadBytesExt>(src: &mut T) -> Result<u32> {
+    src.read_u24::<byteorder::BigEndian>().map_err(From::from)
+}
+
+fn be_u32<T: ReadBytesExt>(src: &mut T) -> Result<u32> {
+    src.read_u32::<byteorder::BigEndian>().map_err(From::from)
+}
+
+fn be_u64<T: ReadBytesExt>(src: &mut T) -> Result<u64> {
+    src.read_u64::<byteorder::BigEndian>().map_err(From::from)
+}
+
+fn write_be_u32<T: WriteBytesExt>(des: &mut T, num: u32) -> Result<()> {
+    des.write_u32::<byteorder::BigEndian>(num)
+        .map_err(From::from)
+}
diff --git a/src/macros.rs b/src/macros.rs
new file mode 100644 (file)
index 0000000..a893f7e
--- /dev/null
@@ -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 (file)
index 0000000..8087e7c
--- /dev/null
@@ -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<F>(size: BoxSize, name: &[u8; 4], func: F) -> Cursor<Vec<u8>>
+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<F>(size: BoxSize, uuid: &[u8; 16], func: F) -> Cursor<Vec<u8>>
+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<F>(size: BoxSize, name: &[u8; 4], version: u8, func: F) -> Cursor<Vec<u8>>
+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<u8> {
+    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<Vec<u8>> {
+    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<Vec<u8>> {
+    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::<u8>::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::<u8>::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<Vec<u8>> {
+    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 (file)
index 0000000..eeb16f8
--- /dev/null
@@ -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<T>(pub T);
+
+impl<T> From<T> for CheckedInteger<T> {
+    fn from(i: T) -> Self {
+        Self(i)
+    }
+}
+
+// Orphan rules prevent a more general implementation, but this suffices
+impl From<CheckedInteger<i64>> for i64 {
+    fn from(checked: CheckedInteger<i64>) -> i64 {
+        checked.0
+    }
+}
+
+impl<T, U: Into<T>> Add<U> for CheckedInteger<T>
+where
+    T: CheckedAdd,
+{
+    type Output = Option<Self>;
+
+    fn add(self, other: U) -> Self::Output {
+        self.0.checked_add(&other.into()).map(Into::into)
+    }
+}
+
+impl<T, U: Into<T>> Sub<U> for CheckedInteger<T>
+where
+    T: CheckedSub,
+{
+    type Output = Option<Self>;
+
+    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<u64> {
+    type Output = Option<CheckedInteger<i64>>;
+
+    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<T: std::cmp::PartialEq> PartialEq<T> for CheckedInteger<T> {
+    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<u64>,
+    /// 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<u64>,
+    /// The time in microseconds when the indexed sample should be displayed.
+    /// Analogous to the concept of presentation time stamp (pts).
+    pub start_composition: CheckedInteger<i64>,
+    /// 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<i64>,
+    /// 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<i64>,
+    /// 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<i64>,
+) -> Option<TryVec<Indice>> {
+    let timescale = match track.timescale {
+        Some(ref t) => TrackTimeScale::<i64>(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::<i64>(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<u32>,
+    cur_offset: i64,
+    ctts_iter: Option<std::slice::Iter<'a, TimeOffset>>,
+    track_id: usize,
+}
+
+impl<'a> Iterator for TimeOffsetIterator<'a> {
+    type Item = i64;
+
+    #[allow(clippy::reversed_empty_ranges)]
+    fn next(&mut self) -> Option<i64> {
+        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<i64> {
+        match self.next() {
+            Some(v) => TrackScaledTime::<i64>(v as i64, self.track_id),
+            _ => TrackScaledTime::<i64>(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<u32>,
+    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<u32> {
+        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<i64> {
+        match self.next() {
+            Some(v) => TrackScaledTime::<i64>(i64::from(v), self.track_id),
+            _ => TrackScaledTime::<i64>(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<SampleToChunk>,
+    stco_offsets: &'a TryVec<u64>,
+) -> 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<u32>,
+    sample_count: u32,
+    stsc_peek_iter: std::iter::Peekable<std::slice::Iter<'a, SampleToChunk>>,
+    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<u32> 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<u32> {
+        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<T, S>(numerator: T, denominator: T, scale2: S) -> Option<T>
+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<T>(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<Microseconds<u64>> {
+    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<T>`
+pub fn track_time_to_us<T>(
+    time: TrackScaledTime<T>,
+    scale: TrackTimeScale<T>,
+) -> Option<Microseconds<T>>
+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::<u64, u64>(17, 3, 1000), Some(5666));
+    let large = 0x4000_0000_0000_0000;
+    assert_eq!(rational_scale::<u64, u64>(large, 2, 2), Some(large));
+    assert_eq!(rational_scale::<u64, u64>(large, 4, 4), Some(large));
+    assert_eq!(rational_scale::<u64, u64>(large, 2, 8), None);
+    assert_eq!(rational_scale::<u64, u64>(large, 8, 4), Some(large / 2));
+    assert_eq!(rational_scale::<u64, u64>(large + 1, 4, 4), Some(large + 1));
+    assert_eq!(rational_scale::<u64, u64>(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 (file)
index 0000000..40652df
Binary files /dev/null and b/tests/1x1-black-alpha-50pct-premultiplied.avif differ
diff --git a/tests/amr_nb_1f.3gp b/tests/amr_nb_1f.3gp
new file mode 100644 (file)
index 0000000..5ef216c
Binary files /dev/null and b/tests/amr_nb_1f.3gp differ
diff --git a/tests/amr_wb_1f.3gp b/tests/amr_wb_1f.3gp
new file mode 100644 (file)
index 0000000..a49f394
Binary files /dev/null and b/tests/amr_wb_1f.3gp differ
diff --git a/tests/av1C-missing-essential.avif b/tests/av1C-missing-essential.avif
new file mode 100644 (file)
index 0000000..c5bf6ee
Binary files /dev/null and b/tests/av1C-missing-essential.avif differ
diff --git a/tests/bad-ipma-flags.avif b/tests/bad-ipma-flags.avif
new file mode 100644 (file)
index 0000000..40012f2
Binary files /dev/null and b/tests/bad-ipma-flags.avif differ
diff --git a/tests/bad-ipma-version.avif b/tests/bad-ipma-version.avif
new file mode 100644 (file)
index 0000000..e8442c1
Binary files /dev/null and b/tests/bad-ipma-version.avif differ
diff --git a/tests/bbb_sunflower_QCIF_30fps_h263_noaudio_1f.3gp b/tests/bbb_sunflower_QCIF_30fps_h263_noaudio_1f.3gp
new file mode 100644 (file)
index 0000000..7e8ff7f
Binary files /dev/null and b/tests/bbb_sunflower_QCIF_30fps_h263_noaudio_1f.3gp differ
diff --git a/tests/clap-basic-1_3x3-to-1x1.avif b/tests/clap-basic-1_3x3-to-1x1.avif
new file mode 100644 (file)
index 0000000..2d59986
Binary files /dev/null and b/tests/clap-basic-1_3x3-to-1x1.avif differ
diff --git a/tests/clusterfuzz-testcase-minimized-mp4-6093954524250112 b/tests/clusterfuzz-testcase-minimized-mp4-6093954524250112
new file mode 100644 (file)
index 0000000..fb6335f
Binary files /dev/null and b/tests/clusterfuzz-testcase-minimized-mp4-6093954524250112 differ
diff --git a/tests/corrupt/bug-1655846.avif b/tests/corrupt/bug-1655846.avif
new file mode 100644 (file)
index 0000000..31c7e42
Binary files /dev/null and b/tests/corrupt/bug-1655846.avif differ
diff --git a/tests/corrupt/bug-1661347.avif b/tests/corrupt/bug-1661347.avif
new file mode 100644 (file)
index 0000000..11b5392
Binary files /dev/null and b/tests/corrupt/bug-1661347.avif differ
diff --git a/tests/corrupt/hdlr-not-first.avif b/tests/corrupt/hdlr-not-first.avif
new file mode 100644 (file)
index 0000000..2011e7b
Binary files /dev/null and b/tests/corrupt/hdlr-not-first.avif differ
diff --git a/tests/corrupt/hdlr-not-pict.avif b/tests/corrupt/hdlr-not-pict.avif
new file mode 100644 (file)
index 0000000..34137ce
Binary files /dev/null and b/tests/corrupt/hdlr-not-pict.avif differ
diff --git a/tests/corrupt/imir-before-clap.avif b/tests/corrupt/imir-before-clap.avif
new file mode 100644 (file)
index 0000000..d02ef72
Binary files /dev/null and b/tests/corrupt/imir-before-clap.avif differ
diff --git a/tests/corrupt/invalid-avif-colr-multiple-nclx.avif b/tests/corrupt/invalid-avif-colr-multiple-nclx.avif
new file mode 100644 (file)
index 0000000..7d2ea0d
Binary files /dev/null and b/tests/corrupt/invalid-avif-colr-multiple-nclx.avif differ
diff --git a/tests/corrupt/invalid-avif-colr-multiple-prof.avif b/tests/corrupt/invalid-avif-colr-multiple-prof.avif
new file mode 100644 (file)
index 0000000..671b8b6
Binary files /dev/null and b/tests/corrupt/invalid-avif-colr-multiple-prof.avif differ
diff --git a/tests/corrupt/invalid-avif-colr-multiple-rICC.avif b/tests/corrupt/invalid-avif-colr-multiple-rICC.avif
new file mode 100644 (file)
index 0000000..e8ced19
Binary files /dev/null and b/tests/corrupt/invalid-avif-colr-multiple-rICC.avif differ
diff --git a/tests/corrupt/invalid-avif-colr-multiple.zip b/tests/corrupt/invalid-avif-colr-multiple.zip
new file mode 100644 (file)
index 0000000..1462988
Binary files /dev/null and b/tests/corrupt/invalid-avif-colr-multiple.zip differ
diff --git a/tests/corrupt/ipma-duplicate-item_id.avif b/tests/corrupt/ipma-duplicate-item_id.avif
new file mode 100644 (file)
index 0000000..ed47ebf
Binary files /dev/null and b/tests/corrupt/ipma-duplicate-item_id.avif differ
diff --git a/tests/corrupt/ipma-duplicate-version-and-flags.avif b/tests/corrupt/ipma-duplicate-version-and-flags.avif
new file mode 100644 (file)
index 0000000..9bc5e76
Binary files /dev/null and b/tests/corrupt/ipma-duplicate-version-and-flags.avif differ
diff --git a/tests/corrupt/ipma-invalid-property-index.avif b/tests/corrupt/ipma-invalid-property-index.avif
new file mode 100644 (file)
index 0000000..e5ecbb9
Binary files /dev/null and b/tests/corrupt/ipma-invalid-property-index.avif differ
diff --git a/tests/corrupt/no-alpha-av1C.avif b/tests/corrupt/no-alpha-av1C.avif
new file mode 100644 (file)
index 0000000..6d674eb
Binary files /dev/null and b/tests/corrupt/no-alpha-av1C.avif differ
diff --git a/tests/corrupt/no-av1C.avif b/tests/corrupt/no-av1C.avif
new file mode 100644 (file)
index 0000000..f46fcb6
Binary files /dev/null and b/tests/corrupt/no-av1C.avif differ
diff --git a/tests/corrupt/no-hdlr.avif b/tests/corrupt/no-hdlr.avif
new file mode 100644 (file)
index 0000000..d75e869
Binary files /dev/null and b/tests/corrupt/no-hdlr.avif differ
diff --git a/tests/corrupt/no-ispe.avif b/tests/corrupt/no-ispe.avif
new file mode 100644 (file)
index 0000000..69f97ca
Binary files /dev/null and b/tests/corrupt/no-ispe.avif differ
diff --git a/tests/corrupt/no-pixi-for-alpha.avif b/tests/corrupt/no-pixi-for-alpha.avif
new file mode 100644 (file)
index 0000000..caf1b7f
Binary files /dev/null and b/tests/corrupt/no-pixi-for-alpha.avif differ
diff --git a/tests/corrupt/no-pixi.avif b/tests/corrupt/no-pixi.avif
new file mode 100644 (file)
index 0000000..9e04a68
Binary files /dev/null and b/tests/corrupt/no-pixi.avif differ
diff --git a/tests/hdlr-nonzero-reserved.avif b/tests/hdlr-nonzero-reserved.avif
new file mode 100644 (file)
index 0000000..e84ba63
Binary files /dev/null and b/tests/hdlr-nonzero-reserved.avif differ
diff --git a/tests/imir-missing-essential.avif b/tests/imir-missing-essential.avif
new file mode 100644 (file)
index 0000000..6baa293
Binary files /dev/null and b/tests/imir-missing-essential.avif differ
diff --git a/tests/irot-missing-essential.avif b/tests/irot-missing-essential.avif
new file mode 100644 (file)
index 0000000..8c62e55
Binary files /dev/null and b/tests/irot-missing-essential.avif differ
diff --git a/tests/multiple-extents.avif b/tests/multiple-extents.avif
new file mode 100644 (file)
index 0000000..dd30699
Binary files /dev/null and b/tests/multiple-extents.avif differ
diff --git a/tests/no-mif1.avif b/tests/no-mif1.avif
new file mode 100644 (file)
index 0000000..1ccbcc4
Binary files /dev/null and b/tests/no-mif1.avif differ
diff --git a/tests/overflow.rs b/tests/overflow.rs
new file mode 100644 (file)
index 0000000..ce5dc52
--- /dev/null
@@ -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 (file)
index 0000000..30e2bff
--- /dev/null
@@ -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<T: std::fmt::Debug>(result: mp4::Result<T>, 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<T: std::fmt::Debug>(result: mp4::Result<T>, 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<std::path::PathBuf> {
+    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 (file)
index 0000000..b192607
Binary files /dev/null and b/tests/valid-alpha.avif differ
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 (file)
index 0000000..b9fe185
Binary files /dev/null and b/tests/valid-avif-colr-nclx-and-prof-and-rICC.avif differ
diff --git a/tests/valid-avif-colr-nclx-and-prof.avif b/tests/valid-avif-colr-nclx-and-prof.avif
new file mode 100644 (file)
index 0000000..683baa7
Binary files /dev/null and b/tests/valid-avif-colr-nclx-and-prof.avif differ
diff --git a/tests/valid-avif-colr-nclx-and-rICC.avif b/tests/valid-avif-colr-nclx-and-rICC.avif
new file mode 100644 (file)
index 0000000..eea6c78
Binary files /dev/null and b/tests/valid-avif-colr-nclx-and-rICC.avif differ
diff --git a/tests/valid-avif-colr-nclx.avif b/tests/valid-avif-colr-nclx.avif
new file mode 100644 (file)
index 0000000..3aa4b8f
Binary files /dev/null and b/tests/valid-avif-colr-nclx.avif differ
diff --git a/tests/valid-avif-colr-prof-and-rICC.avif b/tests/valid-avif-colr-prof-and-rICC.avif
new file mode 100644 (file)
index 0000000..a4609aa
Binary files /dev/null and b/tests/valid-avif-colr-prof-and-rICC.avif differ
diff --git a/tests/valid-avif-colr-prof.avif b/tests/valid-avif-colr-prof.avif
new file mode 100644 (file)
index 0000000..2ee8fa8
Binary files /dev/null and b/tests/valid-avif-colr-prof.avif differ
diff --git a/tests/valid-avif-colr-rICC.avif b/tests/valid-avif-colr-rICC.avif
new file mode 100644 (file)
index 0000000..1da1119
Binary files /dev/null and b/tests/valid-avif-colr-rICC.avif differ
diff --git a/tests/valid.avif b/tests/valid.avif
new file mode 100644 (file)
index 0000000..d576365
Binary files /dev/null and b/tests/valid.avif differ