Compare commits

..

20 Commits
main ... mux

Author SHA1 Message Date
Jordan Rose
479e0bf36f Add mp4san-dump utility for additional testing
Some checks failed
test / format-check (push) Has been cancelled
test / build-and-test (push) Has been cancelled
test / fuzz-afl (push) Has been cancelled
test / fuzz-libfuzzer (push) Has been cancelled
test / check-nightly (push) Has been cancelled
This dumps every sample in the first track to a file, assuming
(without checking) that that track is an H.264 video track with one
NAL unit per sample. This can be compared against the output from

    ffmpeg -i input.mp4 -vcodec copy output.h264

It *will* have differences because it doesn't include the
configuration info ("SPS" and "PPS"), but it should be clear that
those are the only differences.
2024-12-05 15:15:41 -08:00
Jordan Rose
a9bfc4198d Don't require a BufReader for Mp4Box::read_data
This is superfluous when using an in-memory data source, and
potentially harmful if creating a sub-reader from a larger reader.
2024-12-05 15:15:41 -08:00
Jordan Rose
3deef86070 When parsing fails due to trailing data, say how much was left over 2024-12-05 15:15:41 -08:00
Jordan Rose
4c49f3578b Add entries() to stco/co64 and stsc, to go with entries_mut() 2024-12-05 15:15:41 -08:00
Jordan Rose
132a8425e2 Fix example_moov for corrected stsd and added stsz boxes 2024-12-05 15:15:41 -08:00
Jordan Rose
46ab331325 Add stsz box 2024-12-05 15:15:41 -08:00
Lilith Silvestris
a92f41c94f mp4san-test: add remaining mandatory boxes 2024-12-05 15:15:41 -08:00
Jordan Rose
7dc24e0e85 stsd box includes full header 2024-12-05 15:15:41 -08:00
Jessa
74a3177ff9 add stsc and stts boxes 2024-12-05 15:15:41 -08:00
Jessa
c190e85bb3 add derives for Mp4Value and Mp4Prim 2024-12-05 15:15:41 -08:00
Jessa
4315016042 add stsd box 2024-12-05 15:15:41 -08:00
Jessa
66159c7be3 add hdlr box and string values 2024-12-05 15:15:41 -08:00
Jessa
0834aa9223 add tkhd box and box flags values 2024-12-05 15:15:41 -08:00
Jessa
3de0df01f2 add mvhd box; and transform, fixed-point, and nonzero int values 2024-12-05 15:15:41 -08:00
Jessa
f6244d321b add mdhd box and const int values 2024-12-05 15:15:21 -08:00
Jessa
70de41379e derive ParseBox for multi-variant enums
the semantics of this is that an attempt is made to parse the first field of each variant until one succeeds, and that
variant is chosen and the rest of which is parsed.
2024-12-05 15:15:21 -08:00
Jessa
8f45aef9e2 add derivable ParsedBoxes trait for strong typing of children
This replaces the BoxesValidator trait, and allows box definitions to put constraints on whether or not child boxes are
present, and in what numbers. For example, these field types:

T: a child T must be present exactly once
Option<T>: a child T may be present or not
Vec<T>: many children T may be present or not
NonEmpty<T>: at least one child T must be present
2024-12-05 15:15:21 -08:00
Jessa
9f5962a56f use trait const instead of fn for box type in ParseBox 2024-12-05 15:14:38 -08:00
Jessa
83fd823e22 make some error stack messages nicer 2024-12-05 15:14:38 -08:00
Jessa
5be8d3aead clean up proc macro code and use synstructure 2024-12-05 15:14:38 -08:00
59 changed files with 2731 additions and 706 deletions

View File

@ -1,47 +1,14 @@
[workspace]
members = ["cli", "common", "mp4san", "mp4san-derive", "mp4san-test", "mp4san-test-gen", "webpsan", "webpsan-test"]
members = ["cli", "common", "mp4san", "mp4san-derive", "mp4san-dump", "mp4san-test", "mp4san-test-gen", "webpsan", "webpsan-test"]
resolver = "2"
[workspace.package]
version = "0.5.3"
version = "0.5.2"
edition = "2021"
rust-version = "1.66.0"
rust-version = "1.61.0"
repository = "https://github.com/privacyresearchgroup/mp4san"
license = "MIT"
exclude = ["/.github", ".*", "/test-data/"]
[workspace.dependencies]
ac-ffmpeg = "0.18.1"
afl = "0.12.16"
anyhow = "1.0.68"
assert_matches = "1.5.0"
bindgen = "0.69.4"
bitflags = "2.4.0"
bitstream-io = "1.7.0"
bytes = "1.3.0"
cc = "1.0.79"
clap = "4.0.32"
criterion = "0.5.1"
derive-where = "1.1.0"
derive_builder = "0.20.2"
derive_more = "2.0.0"
downcast-rs = "2.0.1"
dyn-clonable = "0.9.0"
env_logger = "0.11.3"
ffmpeg-sys-next = { version = "7.0.0", default-features = false }
futures-util = { version = "0.3.28", default-features = false }
libflate = "2.1.0"
libwebp-sys = "0.9.4"
log = "0.4.17"
num-integer = { version = "0.1.45", default-features = false }
num-traits = { version = "0.2.16", default-features = false }
paste = "1.0.14"
pkg-config = "0.3.26"
proc-macro2 = "1.0"
quote = "1.0"
syn = "2.0"
thiserror = "2.0.12"
uuid = "1.3"

View File

@ -6,9 +6,9 @@ version = "0.1.0"
publish = false
[dependencies]
anyhow = { workspace = true }
clap = { workspace = true, features = ["derive"] }
anyhow = "1.0.68"
clap = { version = "4.0.32", features = ["derive"] }
mp4san = { path = "../mp4san" }
webpsan = { path = "../webpsan" }
env_logger = { workspace = true }
log = { workspace = true }
env_logger = "0.11.3"
log = "0.4.17"

View File

@ -6,7 +6,7 @@ use std::path::PathBuf;
use anyhow::Context;
use clap::{Parser as _, ValueEnum};
use mp4san::{Config, SanitizedMetadata};
use mp4san::SanitizedMetadata;
#[derive(clap::Parser)]
struct Args {
@ -20,9 +20,6 @@ struct Args {
#[clap(long, short = 'o')]
output: Option<PathBuf>,
#[clap(long, short = 'c')]
cumulative_mdat_box_size: Option<u32>,
/// Path to the file to test sanitization on.
file: PathBuf,
}
@ -54,26 +51,23 @@ fn main() -> Result<(), anyhow::Error> {
let mut infile = File::open(&args.file).context("Error opening file")?;
match format {
Format::Mp4 => {
let config = Config { cumulative_mdat_box_size: args.cumulative_mdat_box_size, ..Default::default() };
match mp4san::sanitize_with_config(&mut infile, config).context("Error parsing mp4 file")? {
SanitizedMetadata { metadata: Some(metadata), data } => {
if let Some(output_path) = args.output {
let mut outfile = File::create(output_path).context("Error opening output file")?;
outfile.write(&metadata).context("Error writing output")?;
infile
.seek(io::SeekFrom::Start(data.offset))
.context("Error seeking input")?;
io::copy(&mut infile.take(data.len), &mut outfile).context("Error copying input to output")?;
}
}
SanitizedMetadata { metadata: None, .. } => {
if let Some(output_path) = args.output {
fs::copy(&args.file, output_path).context("Error writing output")?;
}
Format::Mp4 => match mp4san::sanitize(&mut infile).context("Error parsing mp4 file")? {
SanitizedMetadata { metadata: Some(metadata), data } => {
if let Some(output_path) = args.output {
let mut outfile = File::create(output_path).context("Error opening output file")?;
outfile.write(&metadata).context("Error writing output")?;
infile
.seek(io::SeekFrom::Start(data.offset))
.context("Error seeking input")?;
io::copy(&mut infile.take(data.len), &mut outfile).context("Error copying input to output")?;
}
}
}
SanitizedMetadata { metadata: None, .. } => {
if let Some(output_path) = args.output {
fs::copy(&args.file, output_path).context("Error writing output")?;
}
}
},
Format::Webp => {
webpsan::sanitize(infile).context("Error parsing webp file")?;
if let Some(output_path) = args.output {

View File

@ -6,6 +6,6 @@ version = "0.1.0"
publish = false
[dependencies]
env_logger = { workspace = true }
libflate = { workspace = true }
log = { workspace = true }
env_logger = "0.11.3"
libflate = "2.1.0"
log = "0.4.17"

View File

@ -15,7 +15,7 @@ readme = "../README.md"
exclude.workspace = true
[dependencies]
bytes = { workspace = true }
derive_more = { workspace = true, features = ["deref", "deref_mut", "display"] }
futures-util = { workspace = true, features = ["io"] }
thiserror = { workspace = true }
bytes = "1.3.0"
derive_more = "0.99.17"
futures-util = { version = "0.3.28", default-features = false, features = ["io"] }
thiserror = "1.0.38"

View File

@ -40,12 +40,12 @@ pub struct Report<E: ReportableError> {
/// A [`Display`]-able indicating there was extra trailing input after parsing.
#[derive(Clone, Copy, Debug, Display)]
#[display("extra unparsed input")]
#[display(fmt = "extra unparsed input")]
pub struct ExtraUnparsedInput;
/// A [`Display`]-able indicating an error occurred while parsing a certain type.
#[derive(Clone, Copy, Debug, Display)]
#[display("while parsing value of type `{}`", _0)]
#[display(fmt = "while parsing value of type `{}`", _0)]
pub struct WhileParsingType(&'static str);
/// A convenience type alias for a [`Result`](std::result::Result) containing an error wrapped by a [`Report`].
@ -70,7 +70,7 @@ pub struct ReportStack {
/// A null error stack which ignores all data attached to it.
#[derive(Clone, Copy, Debug, Display)]
#[display("")]
#[display(fmt = "")]
pub struct NullReportStack;
/// A trait for error types which can be used in a [`Report`].
@ -95,7 +95,7 @@ pub trait ReportableErrorStack: Display {
//
#[derive(derive_more::Display)]
#[display("{message} at {location}")]
#[display(fmt = "{message} at {location}")]
struct ReportEntry {
message: Box<dyn Display + Send + Sync + 'static>,
location: &'static Location<'static>,

View File

@ -18,7 +18,8 @@ exclude.workspace = true
proc-macro = true
[dependencies]
proc-macro2 = { workspace = true }
syn = { workspace = true }
quote = { workspace = true }
uuid = { workspace = true }
proc-macro2 = "1.0.66"
syn = "2.0.29"
quote = "1.0.33"
uuid = "1.4.1"
synstructure = "0.13.0"

67
mp4san-derive/src/attr.rs Normal file
View File

@ -0,0 +1,67 @@
use proc_macro2::TokenStream;
use quote::{quote, quote_spanned};
use syn::spanned::Spanned;
use syn::{Attribute, Expr, Lit, LitByteStr, Meta, MetaNameValue};
use uuid::Uuid;
pub(crate) fn extract_box_type(attrs: &[Attribute]) -> TokenStream {
let mut iter = attrs.iter().filter(|attr| attr.path().is_ident("box_type"));
let Some(attr) = iter.next() else {
// When emitting compiler errors, no semicolon should be placed after `compile_error!()`:
// doing so will generate extraneous errors (type mismatch errors, Rust parse errors, or the
// like) in addition to the error we intend to emit.
return quote! { std::compile_error!("missing `#[box_type]` attribute") };
};
if let Some(extra_attr) = iter.next() {
return quote_spanned! { extra_attr.span() =>
std::compile_error!("more than one `#[box_type]` attribute is not allowed")
};
}
let lit = match &attr.meta {
Meta::NameValue(MetaNameValue { value: Expr::Lit(expr_lit), .. }) => &expr_lit.lit,
_ => {
return quote_spanned! { attr.span() =>
std::compile_error!("`box_type` attribute must be of the form `#[box_type = ...]`")
}
}
};
match &lit {
Lit::Int(int_lit) => {
let int = match int_lit.base10_parse::<u128>() {
Ok(int) => int,
Err(error) => return error.into_compile_error(),
};
if let Ok(int) = u32::try_from(int) {
let bytes_lit = LitByteStr::new(&int.to_be_bytes(), int_lit.span());
return quote! { mp4san::parse::BoxType::FourCC(mp4san::parse::FourCC { value: *#bytes_lit }) };
} else {
let bytes_lit = LitByteStr::new(&int.to_be_bytes(), int_lit.span());
return quote! { mp4san::parse::BoxType::Uuid(mp4san::parse::BoxUuid { value: *#bytes_lit }) };
}
}
Lit::Str(string_lit) => {
if let Ok(uuid) = Uuid::parse_str(&string_lit.value()) {
let bytes_lit = LitByteStr::new(&uuid.as_u128().to_be_bytes(), string_lit.span());
return quote! { mp4san::parse::BoxType::Uuid(mp4san::parse::BoxUuid { value: *#bytes_lit }) };
} else if string_lit.value().len() == 4 {
let bytes_lit = LitByteStr::new(string_lit.value().as_bytes(), string_lit.span());
return quote! {
mp4san::parse::BoxType::FourCC(mp4san::parse::FourCC { value: *#bytes_lit })
};
}
}
Lit::ByteStr(bytes_lit) => {
if bytes_lit.value().len() == 4 {
return quote! {
mp4san::parse::BoxType::FourCC(mp4san::parse::FourCC { value: *#bytes_lit })
};
}
}
_ => {}
}
quote_spanned! { lit.span() => std::compile_error!(concat!(
r#"malformed `box_type` attribute input: try `"moov"`, `b"moov"`, or `0x6d6f6f76` for a"#,
r#" compact type, or `"a7b5465c-7eac-4caa-b744-bdc340127d37"` or"#,
r#" `0xa7b5465c_7eac_4caa_b744_bdc340127d37` for an extended type"#,
)) }
}

View File

@ -1,212 +1,15 @@
use proc_macro::TokenStream;
mod attr;
mod mp4_prim;
mod mp4_value;
mod parse_box;
mod parse_boxes;
mod parsed_box;
mod util;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, quote_spanned};
use syn::spanned::Spanned;
use syn::{parse_macro_input, Data, DeriveInput, Expr, Ident, Index, Lit};
use uuid::Uuid;
use synstructure::decl_derive;
#[proc_macro_derive(ParseBox, attributes(box_type))]
pub fn derive_parse_box(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let ident = &input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
if matches!(input.data, Data::Enum(_) | Data::Union(_)) {
// This one _does_ need a semicolon though.
return TokenStream::from(quote! {
std::compile_error!("this trait can only be derived for structs");
});
}
let box_type = extract_box_type(&input);
let read_fn = derive_read_fn(&input);
TokenStream::from(quote! {
#[automatically_derived]
impl #impl_generics mp4san::parse::ParseBox for #ident #ty_generics #where_clause {
fn box_type() -> mp4san::parse::BoxType {
#box_type
}
#read_fn
}
})
}
#[proc_macro_derive(ParsedBox, attributes(box_type))]
pub fn derive_parsed_box(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let ident = &input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
if matches!(input.data, Data::Enum(_) | Data::Union(_)) {
// This one _does_ need a semicolon though.
return TokenStream::from(quote! {
std::compile_error!("this trait can only be derived for structs");
});
}
let size = sum_box_size(&input);
let write_fn = derive_write_fn(&input);
TokenStream::from(quote! {
#[automatically_derived]
impl #impl_generics mp4san::parse::ParsedBox for #ident #ty_generics #where_clause {
fn encoded_len(&self) -> u64 {
#size
}
#write_fn
}
})
}
fn derive_write_fn(input: &DeriveInput) -> TokenStream2 {
let write_fields = match &input.data {
Data::Struct(struct_data) => {
let place_expr = struct_data.fields.iter().enumerate().map(|(index, field)| {
if let Some(ident) = &field.ident {
quote_spanned! { field.span() => self.#ident }
} else {
let tuple_index = Index::from(index);
quote_spanned! { field.span() => self.#tuple_index }
}
});
quote! { #( mp4san::parse::Mp4Value::put_buf(&#place_expr, &mut *out); )* }
}
_ => unreachable!(),
};
quote! {
fn put_buf(&self, out: &mut dyn bytes::BufMut) {
#write_fields
}
}
}
fn derive_read_fn(input: &DeriveInput) -> TokenStream2 {
let ident = &input.ident;
match &input.data {
Data::Struct(struct_data) => {
let mut field_ty = Vec::new();
let mut field_ident = Vec::new();
let mut bind_ident = Vec::new();
for (index, field) in struct_data.fields.iter().enumerate() {
field_ty.push(field.ty.clone());
if let Some(ident) = &field.ident {
field_ident.push(quote_spanned! { field.span() => #ident });
bind_ident.push(ident.clone());
} else {
let tuple_index = Index::from(index);
field_ident.push(quote_spanned! { field.span() => #tuple_index });
bind_ident.push(Ident::new(&format!("field_{index}"), Span::mixed_site()));
}
}
quote! {
fn parse(buf: &mut bytes::BytesMut) -> std::result::Result<Self, mp4san::Report<mp4san::parse::ParseError>> {
#(
let #bind_ident: #field_ty =
mp4san::parse::error::ParseResultExt::while_parsing_field(
mp4san::parse::Mp4Value::parse(&mut *buf),
#ident::box_type(),
stringify!(#field_ty),
)?;
)*
if !buf.is_empty() {
return
mp4san::parse::error::ParseResultExt::while_parsing_box(
mp4san::error::ResultExt::attach_printable(
Err(mp4san::parse::ParseError::InvalidInput.into()),
"extra unparsed data",
),
#ident::box_type(),
);
}
std::result::Result::Ok(#ident { #( #field_ident: #bind_ident ),* })
}
}
}
_ => unreachable!(),
}
}
fn extract_box_type(input: &DeriveInput) -> TokenStream2 {
let mut iter = input.attrs.iter().filter(|attr| attr.path().is_ident("box_type"));
let Some(attr) = iter.next() else {
// When emitting compiler errors, no semicolon should be placed after `compile_error!()`:
// doing so will generate extraneous errors (type mismatch errors, Rust parse errors, or the
// like) in addition to the error we intend to emit.
return quote! { std::compile_error!("missing `#[box_type]` attribute") };
};
if let Some(extra_attr) = iter.next() {
return quote_spanned! { extra_attr.span() =>
std::compile_error!("more than one `#[box_type]` attribute is not allowed")
};
}
let lit = match attr.meta.require_name_value().map(|name_value| &name_value.value).ok() {
Some(Expr::Lit(lit)) => &lit.lit,
_ => {
return quote_spanned! { attr.span() =>
std::compile_error!("`box_type` attribute must be of the form `#[box_type = ...]`")
}
}
};
match &lit {
Lit::Int(int_lit) => {
let int = match int_lit.base10_parse::<u128>() {
Ok(int) => int,
Err(error) => return error.into_compile_error(),
};
if let Ok(int) = u32::try_from(int) {
return quote! { mp4san::parse::BoxType::FourCC(mp4san::parse::FourCC { value: #int.to_be_bytes() }) };
} else {
return quote! { mp4san::parse::BoxType::Uuid(mp4san::parse::BoxUuid { value: #int.to_be_bytes() }) };
}
}
Lit::Str(string_lit) => {
let string = string_lit.value();
if let Ok(uuid) = Uuid::parse_str(&string) {
let int = uuid.as_u128();
return quote! { mp4san::parse::BoxType::Uuid(mp4san::parse::BoxUuid { value: #int.to_be_bytes() }) };
} else if string.len() == 4 {
return quote! {
let type_string = #string_lit;
let type_ = std::convert::TryInto::try_into(type_string.as_bytes()).unwrap();
mp4san::parse::BoxType::FourCC(mp4san::parse::FourCC { value: type_ })
};
}
}
Lit::ByteStr(bytes_lit) => {
let bytes = bytes_lit.value();
if bytes.len() == 4 {
return quote! {
mp4san::parse::BoxType::FourCC(mp4san::parse::FourCC { value: *#bytes_lit })
};
}
}
_ => {}
}
quote_spanned! { lit.span() => std::compile_error!(concat!(
r#"malformed `box_type` attribute input: try `"moov"`, `b"moov"`, or `0x6d6f6f76` for a"#,
r#" compact type, or `"a7b5465c-7eac-4caa-b744-bdc340127d37"` or"#,
r#" `0xa7b5465c_7eac_4caa_b744_bdc340127d37` for an extended type"#,
)) }
}
fn sum_box_size(derive_input: &DeriveInput) -> TokenStream2 {
let sum_expr = match &derive_input.data {
Data::Struct(struct_data) => {
let sum_expr = struct_data.fields.iter().enumerate().map(|(index, field)| {
if let Some(ident) = &field.ident {
quote_spanned! { field.span() => mp4san::parse::Mp4Value::encoded_len(&self.#ident) }
} else {
let tuple_index = Index::from(index);
quote_spanned! { field.span() => mp4san::parse::Mp4Value::encoded_len(&self.#tuple_index) }
}
});
quote! { #(+ #sum_expr)* }
}
_ => unreachable!(),
};
quote! {
0 #sum_expr
}
}
decl_derive!([ParseBox, attributes(box_type)] => parse_box::derive);
decl_derive!([ParsedBox, attributes(box_type)] => parsed_box::derive);
decl_derive!([ParseBoxes, attributes(box_type)] => parse_boxes::derive);
decl_derive!([Mp4Prim] => mp4_prim::derive);
decl_derive!([Mp4Value] => mp4_value::derive);

View File

@ -0,0 +1,59 @@
use proc_macro2::TokenStream;
use quote::{quote, quote_spanned};
use syn::spanned::Spanned;
use synstructure::Structure;
use crate::util::StructureExt;
pub(crate) fn derive(input: Structure) -> TokenStream {
let parse = input.parse(|_variant, field, _idx| {
quote_spanned! {
field.span() => Mp4Prim::parse(&mut buf).while_parsing_type()
}
});
let mut variant_encoded_len = input.variants().iter().map(|variant| {
variant.bindings().iter().fold(quote!(0), |acc, binding| {
let ty = &binding.ast().ty;
quote! { #acc + <#ty>::ENCODED_LEN }
})
});
let first_variant_encoded_len = variant_encoded_len.next();
let put_buf = input.each(|binding| quote! { buf.put_mp4_value(#binding); });
let ident = &input.ast().ident;
input.gen_impl(quote! {
use std::prelude::v1::*;
use bytes::{Buf, BufMut, BytesMut};
use mp4san::Report;
use mp4san::error::__ResultExt;
use mp4san::parse::{Mp4Prim, Mp4ValueWriterExt, ParseError};
use mp4san::parse::error::__ParseResultExt;
#(if <#ident>::ENCODED_LEN != #variant_encoded_len {
panic!(concat!(
"error in #[derive(Mp4Prim)] for ",
stringify!(#ident),
": all variants must have equal encoded length",
));
})*
#[automatically_derived]
gen impl Mp4Prim for @Self {
const ENCODED_LEN: u64 = #first_variant_encoded_len;
fn parse<B: Buf + AsRef<[u8]>>(mut buf: B) -> Result<Self, Report<ParseError>> {
if (buf.remaining() as u64) < Self::ENCODED_LEN {
return Err(Report::from(ParseError::TruncatedBox)).while_parsing_type();
}
#parse
}
fn put_buf<B: BufMut>(&self, mut buf: B) {
match *self { #put_buf }
}
}
})
}

View File

@ -0,0 +1,41 @@
use proc_macro2::TokenStream;
use quote::{quote, quote_spanned};
use syn::spanned::Spanned;
use synstructure::Structure;
use crate::util::StructureExt;
pub(crate) fn derive(input: Structure) -> TokenStream {
let parse = input.parse(|_variant, field, _idx| {
quote_spanned! {
field.span() => Mp4Value::parse(&mut *buf).while_parsing_type()
}
});
let encoded_len = input.fold(quote!(0), |acc, binding| quote! { #acc + #binding.encoded_len() });
let put_buf = input.each(|binding| quote! { buf.put_mp4_value(#binding); });
input.gen_impl(quote! {
use std::prelude::v1::*;
use bytes::{BufMut, BytesMut};
use mp4san::Report;
use mp4san::error::__ResultExt;
use mp4san::parse::{Mp4Value, Mp4ValueWriterExt, ParseError};
use mp4san::parse::error::__ParseResultExt;
#[automatically_derived]
gen impl Mp4Value for @Self {
fn parse(buf: &mut BytesMut) -> Result<Self, Report<ParseError>> {
#parse
}
fn encoded_len(&self) -> u64 {
match *self { #encoded_len }
}
fn put_buf<B: BufMut>(&self, mut buf: B) {
match *self { #put_buf }
}
}
})
}

View File

@ -0,0 +1,54 @@
use proc_macro2::TokenStream;
use quote::{quote, quote_spanned};
use syn::spanned::Spanned;
use synstructure::Structure;
use crate::attr::extract_box_type;
use crate::util::StructureExt;
pub(crate) fn derive(input: Structure) -> TokenStream {
let box_type = extract_box_type(&input.ast().attrs);
let ident = &input.ast().ident;
let parse = input.parse(|variant, field, idx| {
let field_ident = &field.ident;
let check_buf_empty = (idx == variant.bindings().len() - 1).then(|| {
quote! {
if !buf.is_empty() {
return Err(Report::from(ParseError::InvalidInput))
.attach_printable(format!("{} bytes of extra unparsed data", buf.len()))
.while_parsing_box(<#ident>::NAME);
}
}
});
quote_spanned! {
field.span() => {
#[allow(clippy::let_and_return)]
let parsed = Mp4Value::parse(&mut *buf).while_parsing_field(<#ident>::NAME, stringify!(#field_ident));
if parsed.is_ok() {
#check_buf_empty
}
parsed
}
}
});
input.gen_impl(quote! {
use std::prelude::v1::*;
use bytes::BytesMut;
use mp4san::Report;
use mp4san::error::ResultExt;
use mp4san::parse::{BoxType, Mp4Value, ParseBox, ParseError, FourCC};
use mp4san::parse::error::ParseResultExt;
#[automatically_derived]
gen impl ParseBox for @Self {
const NAME: BoxType = #box_type;
fn parse(buf: &mut BytesMut) -> Result<Self, Report<ParseError>> {
#parse
}
}
})
}

View File

@ -0,0 +1,255 @@
use proc_macro2::TokenStream;
use quote::{format_ident, quote, quote_spanned};
use syn::spanned::Spanned;
use syn::token::Mut;
use syn::{parse2, Data, DataEnum, DataStruct, DeriveInput, Field, Fields};
use synstructure::{BindStyle, Structure, VariantAst, VariantInfo};
use crate::attr::extract_box_type;
pub(crate) fn derive(input: Structure) -> TokenStream {
if let Data::Union(_) = &input.ast().data {
return quote! {
std::compile_error!("this trait cannot be derived for unions");
};
};
let [variant] = input.variants() else {
return quote! {
std::compile_error!("this trait can only be derived for structs or single-variant enums");
};
};
let box_type = extract_box_type(&input.ast().attrs);
let ref_item @ DeriveInput { ident: ref_item_ident, generics: ref_item_generics, .. } =
&parse2(derive_ref_item(&input, false)).unwrap();
let ref_variant = Structure::new(ref_item).variants()[0].clone();
let ref_mut_item @ DeriveInput { ident: ref_mut_item_ident, generics: ref_mut_item_generics, .. } =
&parse2(derive_ref_item(&input, true)).unwrap();
let ref_mut_variant = Structure::new(ref_mut_item).variants()[0].clone();
let validate_fn_body = derive_parse_fn_body(variant, None, ParseFnKind::Validate);
let parse_fn_body = derive_parse_fn_body(variant, Some(&ref_mut_variant), ParseFnKind::Parse);
let parsed_fn_body = derive_parse_fn_body(variant, Some(&ref_variant), ParseFnKind::Parsed);
let into_iter_fn = derive_into_iter_fn(&input);
let parse_boxes_impl = input.gen_impl(quote! {
use std::vec;
use std::prelude::v1::*;
use mp4san::error::Report;
use mp4san::parse::{AnyMp4Box, Boxes, BoxType, Mp4Box, ParseBoxes, ParseError};
use mp4san::parse::derive::parse_boxes::{Field, Accumulator};
use mp4san::parse::error::__ParseResultExt;
type FieldType<T> = <T as Field>::Type;
type FieldAccumulator<T, U> = <T as Field>::Accumulator<U>;
type FieldAccumulatorUnwrapped<T, U> = <<T as Field>::Accumulator<U> as Accumulator<U>>::Unwrapped;
const NAME: BoxType = #box_type;
#[automatically_derived]
gen impl ParseBoxes for @Self {
type Ref<'a> = #ref_item_ident #ref_item_generics;
type RefMut<'a> = #ref_mut_item_ident #ref_mut_item_generics;
type IntoIter = vec::IntoIter<AnyMp4Box>;
fn validate<'boxes>(boxes: &'boxes mut [AnyMp4Box]) -> Result<(), Report<ParseError>> {
#validate_fn_body
}
fn parse<'boxes>(boxes: &'boxes mut [AnyMp4Box]) -> Result<Self::RefMut<'boxes>, Report<ParseError>>
where
Self: 'boxes,
{
#parse_fn_body
}
fn parsed<'boxes>(boxes: &'boxes [AnyMp4Box]) -> Self::Ref<'boxes>
where
Self: 'boxes,
{
let parsed_impl = |boxes: &'boxes [AnyMp4Box]| -> Result<Self::Ref<'boxes>, Report<ParseError>> {
#parsed_fn_body
};
parsed_impl(boxes).unwrap()
}
#into_iter_fn
}
});
quote! {
#parse_boxes_impl
#[derive(Clone, Debug)]
#[allow(clippy::type_complexity)]
#ref_item
#[derive(Debug)]
#[allow(clippy::type_complexity)]
#ref_mut_item
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ParseFnKind {
Validate,
Parse,
Parsed,
}
fn derive_parse_fn_body(
variant: &VariantInfo<'_>,
return_variant: Option<&VariantInfo<'_>>,
parse_fn_kind: ParseFnKind,
) -> TokenStream {
let declare_field = variant.bindings().iter().map(|binding| {
let Field { ty, ident, .. } = binding.ast();
quote_spanned! { ident.span() => let mut #binding: FieldAccumulator<#ty, _> = Default::default(); }
});
let parse_match_arm = variant.bindings().iter().map(|binding| {
let Field { ty, .. } = binding.ast();
let parse_expr = match parse_fn_kind {
ParseFnKind::Validate => quote! { mp4box.parse_data_as()?.map(drop::<&mut FieldType<#ty>>) },
ParseFnKind::Parse => quote! { mp4box.parse_data_as()? },
ParseFnKind::Parsed => quote! { mp4box.data.parsed() },
};
quote! {
box_type if box_type == <FieldType<#ty>>::NAME && !Accumulator::is_full(&#binding) => {
if let Some(field) = #parse_expr {
Accumulator::push(&mut #binding, field);
}
},
}
});
let unwrap_field = variant.bindings().iter().map(|binding| {
let Field { ty, ident, .. } = binding.ast();
quote_spanned! {
ident.span() =>
let #binding = Accumulator::unwrap(#binding)
.ok_or_else(|| Report::from(ParseError::MissingRequiredBox(<FieldType<#ty>>::NAME)))
.while_parsing_child(NAME, <FieldType<#ty>>::NAME)?;
}
});
let return_value = match return_variant {
Some(return_variant) => return_variant.construct(|_field, idx| &variant.bindings()[idx]),
None => quote!(()),
};
quote! {
#(#declare_field)*
for mp4box in boxes {
match mp4box.box_type() {
#(#parse_match_arm)*
_ => (),
}
}
#(#unwrap_field)*
Ok(#return_value)
}
}
fn derive_into_iter_fn(input: &Structure) -> TokenStream {
let variant_len = input.variants().iter().map(|variant| variant.bindings().len());
let field_count = variant_len.sum::<usize>();
let push_fields = input
.clone()
.bind_with(|_| BindStyle::Move)
.fold(quote!(), |acc, binding| {
let push_fields = quote_spanned! {
binding.ast().span() =>
for mp4box in Field::into_iter(#binding) {
fields.push(AnyMp4Box::from(Mp4Box::with_parsed(Box::new(mp4box))?));
}
};
quote! { #acc #push_fields }
});
quote! {
fn try_into_iter(self) -> Result<Self::IntoIter, Report<ParseError>> {
let mut fields = Vec::with_capacity(#field_count);
match self { #push_fields }
Ok(IntoIterator::into_iter(fields))
}
}
}
fn derive_ref_item(input: &Structure, mutable: bool) -> TokenStream {
let DeriveInput { ident, vis, data, .. } = input.ast();
let ident = match mutable {
false => format_ident!("{ident}Ref", span = ident.span()),
true => format_ident!("{ident}RefMut", span = ident.span()),
};
let lt_a = (!input.variants().iter().all(|variant| variant.bindings().is_empty())).then(|| quote! { 'a });
let path = quote! { mp4san::parse::derive::parse_boxes:: };
let mutability = mutable.then(Mut::default);
let variants = input.variants().iter().fold(quote!(), |acc, variant| {
let VariantAst { ident, discriminant, .. } = variant.ast();
let discriminant = discriminant
.as_ref()
.map(|(eq, expr)| quote! { #eq #expr })
.unwrap_or_default();
let fields = variant.bindings().iter().fold(quote!(), |acc, binding| {
let Field { vis, ident, colon_token, ty, .. } = binding.ast();
let ty_as_field = quote_spanned! { ident.span() => #ty as #path Field };
let ty_ref = quote_spanned! { ident.span() => &'a #mutability <#ty_as_field>::Type };
let declare_field = quote_spanned! {
ident.span() => #vis #ident #colon_token
<<#ty_as_field>::Accumulator<#ty_ref> as #path Accumulator<#ty_ref>>::Unwrapped,
};
quote! { #acc #declare_field }
});
match &input.ast().data {
Data::Enum { .. } => match &variant.ast().fields {
Fields::Unnamed { .. } => quote! { #acc #ident #discriminant(#fields), },
Fields::Named { .. } => quote! { #acc #ident #discriminant { #fields }, },
Fields::Unit { .. } => quote! { #acc #ident #discriminant, },
},
Data::Union { .. } | Data::Struct { .. } => quote! { #acc #fields },
}
});
match data {
Data::Enum(DataEnum { enum_token, .. }) => quote! { #vis #enum_token #ident<#lt_a> { #variants } },
Data::Struct(DataStruct { struct_token, fields, .. }) => match fields {
Fields::Named(_) => quote! { #vis #struct_token #ident<#lt_a> { #variants } },
Fields::Unnamed(_) => quote! { #vis #struct_token #ident<#lt_a>(#variants); },
Fields::Unit => quote! { #vis #struct_token #ident<#lt_a>; },
},
Data::Union(_) => unreachable!(),
}
}
#[cfg(test)]
mod test {
use syn::parse2;
use super::*;
fn test_derive(input: TokenStream) -> TokenStream {
derive(Structure::new(&parse2(input).unwrap()))
}
#[test]
fn empty_struct() {
test_derive(quote! {
struct Empty {}
});
}
#[test]
fn unit_struct() {
test_derive(quote! {
struct Empty;
});
}
}

View File

@ -0,0 +1,35 @@
use proc_macro2::TokenStream;
use quote::{quote, quote_spanned};
use syn::spanned::Spanned;
use synstructure::Structure;
pub(crate) fn derive(input: Structure) -> TokenStream {
let size = input.fold(0u64, |acc, binding| quote! { #acc + Mp4Value::encoded_len(#binding) });
let write_fn = derive_write_fn(&input);
input.gen_impl(quote! {
use std::prelude::v1::*;
use mp4san::parse::{Mp4Value, ParsedBox};
#[automatically_derived]
gen impl ParsedBox for @Self {
fn encoded_len(&self) -> u64 {
match *self { #size }
}
#write_fn
}
})
}
fn derive_write_fn(input: &Structure) -> TokenStream {
let write_fields = input.each(|binding| {
quote_spanned! { binding.ast().span() => Mp4Value::put_buf(#binding, &mut *out); }
});
quote! {
fn put_buf(&self, out: &mut dyn bytes::BufMut) {
match *self { #write_fields }
}
}
}

52
mp4san-derive/src/util.rs Normal file
View File

@ -0,0 +1,52 @@
use proc_macro2::TokenStream;
use quote::{quote, quote_spanned, ToTokens};
use syn::spanned::Spanned;
use syn::Field;
use synstructure::{Structure, VariantInfo};
pub trait StructureExt {
fn construct<F: FnMut(&VariantInfo, usize) -> T, T: ToTokens>(&self, fun: F) -> TokenStream;
fn parse<F: FnMut(&VariantInfo, &Field, usize) -> T, T: ToTokens>(&self, fun: F) -> TokenStream;
}
impl StructureExt for Structure<'_> {
fn construct<F: FnMut(&VariantInfo, usize) -> T, T: ToTokens>(&self, mut fun: F) -> TokenStream {
let variants = self.variants().iter().enumerate();
variants.fold(quote!({}), |acc, (variant_idx, variant)| {
let parse_variant = fun(variant, variant_idx);
if variant_idx != self.variants().len() - 1 {
quote! { #acc let _ = { #parse_variant }; }
} else {
quote! { #acc #parse_variant }
}
})
}
fn parse<F: FnMut(&VariantInfo, &Field, usize) -> T, T: ToTokens>(&self, mut fun: F) -> TokenStream {
self.construct(|variant, _idx| {
let parse_expr = variant.construct(|field, idx| {
let parse_expr = fun(variant, field, idx);
match idx {
0 => quote_spanned! {
field.span() => {
let parse_result = { #parse_expr };
match parse_result {
Ok(parsed) => parsed,
Err(err) => break 'parse_variant Err::<Self, _>(err),
}
}
},
_ => quote_spanned! { field.span() => { #parse_expr }? },
}
});
let label = (!variant.bindings().is_empty()).then(|| quote! { 'parse_variant: });
quote_spanned! {
variant.ast().ident.span() => #label {
let parsed = { #parse_expr };
#[allow(clippy::needless_return)]
return Ok(parsed);
}
}
})
}
}

16
mp4san-dump/Cargo.toml Normal file
View File

@ -0,0 +1,16 @@
[package]
name = "mp4san-dump"
description = "Sample utilities built on top of mp4san to synchronously dump frames"
version.workspace = true
edition.workspace = true
publish = false
[dependencies]
mp4san = { path = "../mp4san" }
mediasan-common = { path = "../common" }
bytes = "1.3.0"
env_logger = "0.10.0"
futures-util = { version = "0.3.28", default-features = false, features = ["io"] }
log = "0.4.17"

218
mp4san-dump/src/lib.rs Normal file
View File

@ -0,0 +1,218 @@
//! WARNING: This is not a 100% correct implementation of a frame dumper for H.264 in MPEG-4.
//!
//! Many many things were skipped and/or hardcoded. Do not use this as a reference, only a starting
//! point.
use std::io;
use std::ops::Range;
use bytes::Buf as _;
use futures_util::{pin_mut, FutureExt as _};
use mediasan_common::{bail_attach, ensure_attach, report_attach, SeekSkipAdapter};
use mp4san::parse::{
ArrayEntry, BoxHeader, BoxType, FtypBox, MoovBox, Mp4Box, ParseBox, ParseError, ParsedBox, StscEntry, TrakBox,
};
use mp4san::{Error, COMPATIBLE_BRAND};
// Note: modified version of mp4san::sanitize_async_with_config; should eventually be folded back
// together with that.
pub fn parse(input: &[u8]) -> Result<Mp4Box<MoovBox>, Error> {
let mut reader = io::Cursor::new(input);
let mut mdat_seen = false;
let mut ftyp_seen = false;
let mut moov: Option<Mp4Box<MoovBox>> = None;
while reader.position() != reader.get_ref().len() as u64 {
let start_pos = reader.position();
let header = BoxHeader::parse(&mut reader).expect("valid header");
match header.box_type() {
name @ (BoxType::FREE | BoxType::SKIP) => {
skip_box(&mut reader, &header).expect("can skip in Cursor");
log::info!("{name} @ 0x{start_pos:08x}");
}
BoxType::FTYP => {
assert!(!ftyp_seen, "multiple ftyp boxes");
let mut read_ftyp = read_data_sync(&mut reader, header, 1024).expect("valid box");
let ftyp_data: &mut FtypBox = read_ftyp.data.parse().expect("valid ftyp");
let compatible_brand_count = ftyp_data.compatible_brands().len();
let FtypBox { major_brand, minor_version, .. } = ftyp_data;
log::info!("ftyp @ 0x{start_pos:08x}: {major_brand} version {minor_version}, {compatible_brand_count} compatible brands");
ensure_attach!(
ftyp_data.compatible_brands().any(|b| b == COMPATIBLE_BRAND),
ParseError::UnsupportedFormat(ftyp_data.major_brand)
);
ftyp_seen = true;
}
// NB: ISO 14496-12-2012 specifies a default ftyp, but we don't currently use it. The spec says that it
// contains a single compatible brand, "mp41", and notably not "isom" which is the ISO spec we follow for
// parsing now. This implies that there's additional stuff in "mp41" which is not in "isom". "mp41" is also
// very old at this point, so it'll require additional research/work to be able to parse/remux it.
_ if !ftyp_seen => {
bail_attach!(ParseError::InvalidBoxLayout, "ftyp is not the first significant box");
}
BoxType::MDAT => {
mdat_seen = true;
skip_box(&mut reader, &header).expect("can skip in Cursor");
log::info!("mdat @ 0x{start_pos:08x}");
}
BoxType::MOOV => {
let mut read_moov: Mp4Box<MoovBox> =
read_data_sync(&mut reader, header, 1024 * 1024).expect("can read box");
let children = read_moov.data.parse().expect("valid moov").parsed_mut();
let chunk_count = children.tracks.iter().map(|trak| trak.co().entry_count()).sum::<u32>();
let trak_count = children.tracks.len();
log::info!("moov @ 0x{start_pos:08x}: {trak_count} traks {chunk_count} chunks");
moov = Some(read_moov);
}
name => {
skip_box(&mut reader, &header).expect("can skip in Cursor");
log::info!("{name} @ 0x{start_pos:08x}");
}
}
}
if !ftyp_seen {
bail_attach!(ParseError::MissingRequiredBox(BoxType::FTYP));
}
if !mdat_seen {
bail_attach!(ParseError::MissingRequiredBox(BoxType::MDAT));
}
let Some(moov) = moov else {
bail_attach!(ParseError::MissingRequiredBox(BoxType::MOOV));
};
Ok(moov)
}
/// Skip a box's data assuming its header has already been read.
fn skip_box(reader: &mut impl io::Seek, header: &BoxHeader) -> Result<(), io::Error> {
match header.box_data_size().expect("valid header") {
Some(box_size) => reader.seek(io::SeekFrom::Current(box_size as i64))?,
None => reader.seek(io::SeekFrom::End(0))?,
};
Ok(())
}
/// Read a box's data.
fn read_data_sync<T, R>(reader: &mut R, header: BoxHeader, max_size: u64) -> Result<Mp4Box<T>, Error>
where
R: std::io::Read + std::io::Seek,
T: ParseBox + ParsedBox,
{
let async_reader = SeekSkipAdapter(futures_util::io::AllowStdIo::new(reader));
pin_mut!(async_reader);
Mp4Box::read_data(async_reader, header, max_size)
.now_or_never()
.expect("only awaits reader")
}
/// Iterates through every sample in the track in order, ignoring timecodes and edit lists.
pub fn for_each_sample(
track: &TrakBox,
full_data: &[u8],
mut process: impl FnMut(&[u8]) -> Result<(), Error>,
) -> Result<(), Error> {
let samples = track.parsed().media.parsed().info.parsed().samples;
let mut sample_to_chunk_walker = SampleToChunkWalker::new(samples.parsed().sample_to_chunk.entries());
let offset_for_chunk = |i| {
Ok::<usize, Error>(match samples.parsed().chunk_offsets {
mp4san::parse::StblCoRef::Stco(stco) => stco
.entries()
.nth(i as usize - 1)
.ok_or_else(|| report_attach!(ParseError::InvalidInput))?
.get()? as usize,
mp4san::parse::StblCoRef::Co64(co64) => co64
.entries()
.nth(i as usize - 1)
.ok_or_else(|| report_attach!(ParseError::InvalidInput))?
.get()? as usize,
})
};
let mut prev_chunk_index = 0;
let mut current_chunk_data: &[u8] = &[];
for (sample_size, sample_index) in samples.parsed().sample_sizes.sample_sizes().zip(0..) {
let sample_size = sample_size?;
let ChunkInfo { chunk_index, num_samples_in_chunk: _ } = sample_to_chunk_walker.chunk_info_for(sample_index);
if chunk_index != prev_chunk_index {
current_chunk_data = &full_data[offset_for_chunk(chunk_index)?..];
prev_chunk_index = chunk_index;
}
let current_sample = &current_chunk_data[..sample_size as usize];
current_chunk_data.advance(sample_size as usize);
process(current_sample)?;
}
Ok(())
}
#[derive(PartialEq, Eq)]
struct ChunkInfo {
pub chunk_index: u32,
pub num_samples_in_chunk: u32,
}
/// Wraps an iterator over stsc ("sample-to-chunk") entries to allow looking up chunks for given
/// sample indexes.
///
/// Samples must be accessed in increasing order, but skipping is allowed. This type expects sample
/// indexes to be zero-indexed.
struct SampleToChunkWalker<T: Iterator> {
entry_iter: std::iter::Peekable<T>,
chunks_in_entry: Range<u32>,
num_samples_in_chunk: u32,
samples_seen_so_far: usize,
}
impl<'a, T> SampleToChunkWalker<T>
where
T: Iterator<Item = ArrayEntry<'a, StscEntry>>,
{
fn new(iter: T) -> Self {
Self { entry_iter: iter.peekable(), chunks_in_entry: 0..0, num_samples_in_chunk: 0, samples_seen_so_far: 0 }
}
fn advance_one_chunk(&mut self) {
self.samples_seen_so_far += self.num_samples_in_chunk as usize;
_ = self.chunks_in_entry.next();
if self.chunks_in_entry.is_empty() {
let next_entry = self
.entry_iter
.next()
.expect("has more entries")
.get()
.expect("valid sample-to-chunk entry");
let upper_bound = self.entry_iter.peek().map_or(u32::MAX, |following_entry| {
following_entry.get().expect("valid sample-to-chunk entry").first_chunk
});
self.chunks_in_entry = next_entry.first_chunk..upper_bound;
self.num_samples_in_chunk = next_entry.samples_per_chunk;
}
}
fn chunk_info_for(&mut self, sample_index: usize) -> ChunkInfo {
assert!(
sample_index >= self.samples_seen_so_far,
"searching backwards is not implemented"
);
while sample_index - self.samples_seen_so_far >= self.num_samples_in_chunk as usize {
self.advance_one_chunk();
}
ChunkInfo { chunk_index: self.chunks_in_entry.start, num_samples_in_chunk: self.num_samples_in_chunk }
}
}

41
mp4san-dump/src/main.rs Normal file
View File

@ -0,0 +1,41 @@
//! WARNING: This is not a 100% correct implementation of a frame dumper for H.264 in MPEG-4.
//!
//! Many many things were skipped and/or hardcoded. Do not use this as a reference, only a starting
//! point.
use std::io::Read as _;
use std::io::{self, Write};
use bytes::Buf;
use mp4san::parse::MoovBox;
const NAL_HEADER: &[u8] = &[0, 0, 0, 1];
pub fn main() {
env_logger::init();
let mut input = Vec::with_capacity(100 * 1024);
io::stdin().read_to_end(&mut input).expect("can read stdin");
let moov = mp4san_dump::parse(&input).expect("valid input");
let moov_children = moov.data.parsed::<MoovBox>().expect("parsed moov box already").parsed();
// FIXME: The first track isn't always the video track.
if let Some(track) = moov_children.tracks.get(0) {
mp4san_dump::for_each_sample(track, &input, |mut sample| {
// FIXME: not all NAL lengths use four bytes
let nal_length = sample.get_u32() as usize;
assert_eq!(
nal_length,
sample.len(),
"NAL split across samples, or multiple NALs in a sample"
);
std::io::stdout().write_all(NAL_HEADER).expect("can write to stdout");
std::io::stdout().write_all(sample).expect("can write to stdout");
Ok(())
})
.expect("valid parsed input");
}
}

View File

@ -6,9 +6,9 @@ version = "0.1.0"
publish = false
[dependencies]
anyhow = { workspace = true }
clap = { workspace = true, features = ["derive"] }
env_logger = { workspace = true }
libflate = { workspace = true }
log = { workspace = true }
anyhow = "1.0.68"
clap = { version = "4.0.32", features = ["derive"] }
env_logger = "0.11.3"
libflate = "2.1.0"
log = "0.4.17"
mp4san = { path = "../mp4san" }

View File

@ -11,12 +11,12 @@ ffmpeg = ["dep:ac-ffmpeg", "dep:bindgen", "dep:cc", "dep:ffmpeg-sys-next"]
gpac = ["dep:bindgen", "dep:cc", "dep:pkg-config"]
[dependencies]
ac-ffmpeg = { workspace = true, optional = true }
ffmpeg-sys-next = { workspace = true, default-features = false, features = ["avformat"], optional = true }
log = { workspace = true }
thiserror = { workspace = true }
ac-ffmpeg = { version = "0.18.1", optional = true }
ffmpeg-sys-next = { version = "7.0.0", default-features = false, features = ["avformat"], optional = true }
log = "0.4.17"
thiserror = "1.0.40"
[build-dependencies]
bindgen = { workspace = true, optional = true }
cc = { workspace = true, optional = true }
pkg-config = { workspace = true, optional = true }
bindgen = { version = "0.69.4", optional = true }
cc = { version = "1.0.79", optional = true }
pkg-config = { version = "0.3.26", optional = true }

View File

@ -117,35 +117,139 @@ pub fn example_mdat() -> Vec<u8> {
pub fn example_moov() -> Vec<u8> {
const EXAMPLE_MOOV: &[&[u8]] = &[
&[0, 0, 0, 56], // box size
b"moov", // box type
&[0, 0, 1, 133], // box size
b"moov", // box type
//
// trak box (inside moov box)
//
&[0, 0, 0, 48], // box size
&[0, 0, 1, 17], // box size
b"trak", // box type
//
// tkhd box (inside trak box)
//
&[0, 0, 0, 92], // box size
b"tkhd", // box type
&[0, 0, 0, 0], // box version & flags
b"\x64\x18\xa0\x50", // creation time
b"\x64\x18\xa0\x50", // modification time
&[0, 0, 0, 1], // track ID
&[0, 0, 0, 0], // reserved
&[0, 0, 0, 0], // duration
&[0; 8], // reserved
&[0, 0], // layer
&[0, 0], // alternate group
&[0, 0], // volume
&[0, 0], // reserved
&[0, 1, 0, 0], // matrix[0]
&[0, 0, 0, 0], // matrix[1]
&[0, 0, 0, 0], // matrix[2]
&[0, 0, 0, 0], // matrix[3]
&[0, 1, 0, 0], // matrix[4]
&[0, 0, 0, 0], // matrix[5]
&[0, 0, 0, 0], // matrix[6]
&[0, 0, 0, 0], // matrix[7]
&[64, 0, 0, 0], // matrix[8]
&[0, 0, 0, 0], // width
&[0, 0, 0, 0], // height
//
// mdia box (inside trak box)
//
&[0, 0, 0, 40], // box size
b"mdia", // box type
&[0, 0, 0, 173], // box size
b"mdia", // box type
//
// mdhd box (inside mdia box)
//
&[0, 0, 0, 32], // box size
b"mdhd", // box type
&[0, 0, 0, 0], // box version & flags
b"\x64\x18\xa0\x50", // creation time
b"\x64\x18\xa0\x50", // modification time
&[0, 0, 2, 88], // time scale
&[0, 0, 0, 0], // duration
b"\x55\xc4", // padding & language
&[0, 0], // pre defined
//
// hdlr box (inside mdia box)
//
&[0, 0, 0, 33], // box size
b"hdlr", // box type
&[0, 0, 0, 0], // box version & flags
&[0, 0, 0, 0], // pre defined
b"vide", // handler type
&[0; 12], // reserved
b"\0", // name
//
// minf box (inside mdia box)
//
&[0, 0, 0, 32], // box size
b"minf", // box type
&[0, 0, 0, 100], // box size
b"minf", // box type
//
// stbl box (inside minf box)
//
&[0, 0, 0, 24], // box size
&[0, 0, 0, 92], // box size
b"stbl", // box type
//
// stsd box (inside stbl box)
//
&[0, 0, 0, 16], // box size
b"stsd", // box type
&[0, 0, 0, 0], // box version & flags
&[0, 0, 0, 0], // entry count
//
// stts box (inside stbl box)
//
&[0, 0, 0, 16], // box size
b"stts", // box type
&[0, 0, 0, 0], // box version & flags
&[0, 0, 0, 0], // entry count
//
// stsc box (inside stbl box)
//
&[0, 0, 0, 16], // box size
b"stsc", // box type
&[0, 0, 0, 0], // box version & flags
&[0, 0, 0, 0], // entry count
//
// stco box (inside stbl box)
//
&[0, 0, 0, 16], // box size
b"stco", // box type
&[0, 0, 0, 0], // box version & flags
&[0, 0, 0, 0], // entry count
//
// stsz box (inside stbl box)
//
&[0, 0, 0, 20], // box size
b"stsz", // box type
&[0, 0, 0, 0], // box version & flags
&[0, 0, 0, 0], // sample size
&[0, 0, 0, 0], // entry count
//
//
// mvhd box (inside moov box)
//
&[0, 0, 0, 108], // box size
b"mvhd", // box type
&[0, 0, 0, 0], // box version & flags
b"\x64\x18\xa0\x50", // creation time
b"\x64\x18\xa0\x50", // modification time
&[0, 0, 2, 88], // time scale
&[0, 0, 0, 0], // duration
&[0, 1, 0, 0], // rate
&[1, 0], // volume
&[0, 0], // reserved
&[0; 8], // reserved
&[0, 1, 0, 0], // matrix[0]
&[0, 0, 0, 0], // matrix[1]
&[0, 0, 0, 0], // matrix[2]
&[0, 0, 0, 0], // matrix[3]
&[0, 1, 0, 0], // matrix[4]
&[0, 0, 0, 0], // matrix[5]
&[0, 0, 0, 0], // matrix[6]
&[0, 0, 0, 0], // matrix[7]
&[64, 0, 0, 0], // matrix[8]
&[0; 24], // pre defined
&[255, 255, 255, 255], // next track ID
];
EXAMPLE_MOOV.concat()
}

View File

@ -15,20 +15,25 @@ readme = "README.md"
exclude.workspace = true
[dependencies]
bytes = { workspace = true }
derive-where = { workspace = true }
derive_builder = { workspace = true }
derive_more = { workspace = true, features = ["display", "from"] }
downcast-rs = { workspace = true }
dyn-clonable = { workspace = true }
futures-util = { workspace = true, features = ["io"] }
log = { workspace = true }
mediasan-common = { path = "../common", version = "=0.5.3" }
mp4san-derive = { path = "../mp4san-derive", version = "=0.5.3" }
paste = { workspace = true }
thiserror = { workspace = true }
bytes = "1.3.0"
derive-where = "1.1.0"
derive_builder = "0.20.0"
derive_more = "0.99.17"
downcast-rs = "1.2.0"
dyn-clonable = "0.9.0"
# fixed 1.28.0 requires rustc 1.79.0 or later
fixed = { version = "<1.28.0", default-features = false }
futures-util = { version = "0.3.28", default-features = false, features = ["io"] }
half = { version = "1.8.2", default-features = false }
log = "0.4.17"
mediasan-common = { path = "../common", version = "=0.5.2" }
mp4san-derive = { path = "../mp4san-derive", version = "=0.5.2" }
nalgebra = { version = "0.32.3", default-features = false }
nonempty = "0.8.1"
paste = "1.0.14"
thiserror = "1.0.38"
[dev-dependencies]
assert_matches = { workspace = true }
assert_matches = "1.5.0"
mediasan-common-test = { path = "../common-test" }
mp4san-test = { path = "../mp4san-test" }

View File

@ -71,7 +71,7 @@ use mediasan_common::AsyncSkipExt;
use crate::error::Report;
use crate::parse::error::{MultipleBoxes, WhileParsingBox};
use crate::parse::{BoxHeader, BoxType, FourCC, FtypBox, MoovBox, Mp4Box, Mp4Value, ParseError, StblCoMut};
use crate::parse::{BoxHeader, BoxType, FourCC, FtypBox, MoovBox, Mp4Box, Mp4Value, ParseError, StblCoRefMut};
//
// public types
@ -90,29 +90,6 @@ pub struct Config {
/// The default is 1 GiB.
#[builder(default = "1024 * 1024 * 1024")]
pub max_metadata_size: u64,
/// The cumulative MDAT box size
///
/// The value is tightly associated with a specific
/// use case scenario in which the transcoder internally
/// generates a sequence of MDAT boxes, but delivers
/// them compounded as a single monolythic MDAT box.
///
/// In order to avoid constantly updating the single
/// MDAT box size (which may be impossible when the
/// payload is continually encrypted and written to
/// the file), the transcoder instead does the following:
/// a) writes the MDAT box size to be equal to the
/// fixed zero value
/// b) keeps accumulating the box size and passes
/// it as config argument to mp4sanitizer
///
/// IMPORTANT: given the special circumstances of
/// the use case scenario of transcoding on mobile
/// devices, the MDAT box size is expected to not
/// exceed the 32-bit max bytes limit. Hence the
/// cumulative_mdat_box_size is a 32-bit value
#[builder(default = None)]
pub cumulative_mdat_box_size: Option<u32>,
}
/// Sanitized metadata returned by the sanitizer.
@ -143,7 +120,7 @@ pub const COMPATIBLE_BRAND: FourCC = FourCC { value: *b"isom" };
//
#[derive(Clone, Copy, Debug, Display)]
#[display("box data too large: {} > {}", _0, _1)]
#[display(fmt = "box data too large: {} > {}", _0, _1)]
struct BoxDataTooLarge(u64, u64);
const MAX_FTYP_SIZE: u64 = 1024;
@ -283,7 +260,7 @@ pub async fn sanitize_async_with_config<R: AsyncRead + AsyncSkip>(
while !reader.as_mut().fill_buf().await?.is_empty() {
let start_pos = reader.as_mut().stream_position().await?;
let mut header = BoxHeader::read(&mut reader)
let header = BoxHeader::read(&mut reader)
.await
.map_eof(|_| Error::Parse(report_attach!(ParseError::TruncatedBox, "while parsing box header")))?;
@ -329,12 +306,6 @@ pub async fn sanitize_async_with_config<R: AsyncRead + AsyncSkip>(
}
BoxType::MDAT => {
if let Ok(None) = header.box_data_size() {
if let Some(t) = config.cumulative_mdat_box_size {
header.overwrite_size(t);
}
}
let box_size = skip_box(reader.as_mut(), &header).await? + header.encoded_len();
log::info!("mdat @ 0x{start_pos:08x}: {box_size} bytes");
@ -352,14 +323,12 @@ pub async fn sanitize_async_with_config<R: AsyncRead + AsyncSkip>(
}
BoxType::MOOV => {
let mut read_moov = Mp4Box::read_data(reader.as_mut(), header, config.max_metadata_size).await?;
let mut read_moov =
Mp4Box::<MoovBox>::read_data(reader.as_mut(), header, config.max_metadata_size).await?;
let children = read_moov.data.parse()?.parsed_mut();
let moov_data: &mut MoovBox = read_moov.data.parse()?;
let trak_chunk_counts = moov_data
.traks()
.map(|trak| Ok::<_, Report<_>>(trak?.co_mut()?.entry_count()));
let chunk_count = trak_chunk_counts.reduce(|a, b| Ok(a? + b?)).unwrap_or(Ok(0))?;
let trak_count = moov_data.traks().count();
let chunk_count = children.tracks.iter().map(|trak| trak.co().entry_count()).sum::<u32>();
let trak_count = children.tracks.len();
log::info!("moov @ 0x{start_pos:08x}: {trak_count} traks {chunk_count} chunks");
moov = Some(read_moov);
@ -434,25 +403,23 @@ pub async fn sanitize_async_with_config<R: AsyncRead + AsyncSkip>(
log::info!("metadata: 0x{metadata_len:08x} bytes; displacing chunk offsets by 0x{mdat_displacement:08x}");
for trak in &mut moov.data.parse()?.traks() {
let co = trak?.co_mut()?;
if let StblCoMut::Stco(stco) = co {
for mut entry in &mut stco.entries_mut() {
let value = entry.get().unwrap_or_else(|_| unreachable!());
entry.set(
checked_add_signed(value, mdat_displacement).ok_or_else(|| {
for trak in moov.data.parse()?.parsed_mut().tracks.iter_mut() {
match trak.co_mut() {
StblCoRefMut::Stco(stco) => {
for mut entry in &mut stco.entries_mut() {
let value = entry.get().unwrap_or_else(|_| unreachable!());
entry.set(checked_add_signed(value, mdat_displacement).ok_or_else(|| {
report_attach!(ParseError::InvalidInput, "chunk offset not within mdat")
})?,
);
})?);
}
}
} else if let StblCoMut::Co64(co64) = co {
for mut entry in &mut co64.entries_mut() {
let value = entry.get().unwrap_or_else(|_| unreachable!());
entry.set(
checked_add_signed(value, mdat_displacement.into()).ok_or_else(|| {
StblCoRefMut::Co64(co64) => {
for mut entry in &mut co64.entries_mut() {
let value = entry.get().unwrap_or_else(|_| unreachable!());
entry.set(checked_add_signed(value, mdat_displacement.into()).ok_or_else(|| {
report_attach!(ParseError::InvalidInput, "chunk offset not within mdat")
})?,
);
})?);
}
}
}
}
@ -594,13 +561,13 @@ mod test {
let mut test = test_mp4().mdat_data(vec![]).clone();
let test_data_len = test.mdat_data_len(u64::MAX - 16).build().data.len() as u64;
let test = test.mdat_data_len(u64::MAX - test_data_len + 1).build();
sanitize(test).unwrap_err();
dbg!(sanitize(test).unwrap_err());
}
#[test]
fn box_size_overflow() {
let test = test_mp4().mdat_data_len(u64::MAX - 16).build();
sanitize(test).unwrap_err();
dbg!(sanitize(test).unwrap_err());
}
#[test]
@ -613,7 +580,7 @@ mod test {
let test = test_mp4()
.ftyp(test_ftyp().compatible_brands(compatible_brands).clone())
.build();
assert_matches!(sanitize(test).unwrap_err(), Error::Parse(err) => {
assert_matches!(dbg!(sanitize(test).unwrap_err()), Error::Parse(err) => {
assert_matches!(err.into_inner(), ParseError::InvalidInput);
});
}
@ -635,7 +602,7 @@ mod test {
.build();
let test = test_spec.build();
test.sanitize_ok();
assert_matches!(sanitize_with_config(test, config).unwrap_err(), Error::Parse(err) => {
assert_matches!(dbg!(sanitize_with_config(test, config).unwrap_err()), Error::Parse(err) => {
assert_matches!(err.into_inner(), ParseError::InvalidInput);
});
}
@ -648,7 +615,7 @@ mod test {
#[test]
fn no_ftyp() {
let test = test_mp4().boxes(&[MOOV, MDAT][..]).build();
assert_matches!(sanitize(test).unwrap_err(), Error::Parse(err) => {
assert_matches!(dbg!(sanitize(test).unwrap_err()), Error::Parse(err) => {
assert_matches!(err.into_inner(), ParseError::InvalidBoxLayout);
});
}
@ -656,7 +623,7 @@ mod test {
#[test]
fn multiple_ftyp() {
let test = test_mp4().boxes(&[FTYP, FTYP, MOOV, MDAT][..]).build();
assert_matches!(sanitize(test).unwrap_err(), Error::Parse(err) => {
assert_matches!(dbg!(sanitize(test).unwrap_err()), Error::Parse(err) => {
assert_matches!(err.into_inner(), ParseError::InvalidBoxLayout);
});
}
@ -670,7 +637,7 @@ mod test {
#[test]
fn ftyp_not_first_significant_box() {
let test = test_mp4().boxes(&[MOOV, FTYP, MDAT][..]).build();
assert_matches!(sanitize(test).unwrap_err(), Error::Parse(err) => {
assert_matches!(dbg!(sanitize(test).unwrap_err()), Error::Parse(err) => {
assert_matches!(err.into_inner(), ParseError::InvalidBoxLayout);
});
}
@ -678,7 +645,7 @@ mod test {
#[test]
fn no_moov() {
let test = test_mp4().boxes(&[FTYP, MDAT][..]).build();
assert_matches!(sanitize(test).unwrap_err(), Error::Parse(err) => {
assert_matches!(dbg!(sanitize(test).unwrap_err()), Error::Parse(err) => {
assert_matches!(err.into_inner(), ParseError::MissingRequiredBox(MOOV));
});
}
@ -686,7 +653,7 @@ mod test {
#[test]
fn no_mdat() {
let test = test_mp4().boxes(&[FTYP, MOOV][..]).build();
assert_matches!(sanitize(test).unwrap_err(), Error::Parse(err) => {
assert_matches!(dbg!(sanitize(test).unwrap_err()), Error::Parse(err) => {
assert_matches!(err.into_inner(), ParseError::MissingRequiredBox(MDAT));
});
}
@ -726,7 +693,7 @@ mod test {
#[test]
fn uuid() {
let test = test_mp4().boxes(&[FTYP, MOOV, TEST_UUID, MDAT][..]).build();
assert_matches!(sanitize(test).unwrap_err(), Error::Parse(err) => {
assert_matches!(dbg!(sanitize(test).unwrap_err()), Error::Parse(err) => {
assert_matches!(err.into_inner(), ParseError::UnsupportedBox(TEST_UUID));
});
}
@ -736,7 +703,7 @@ mod test {
let test = test_mp4()
.ftyp(test_ftyp().major_brand(MP41).add_compatible_brand(MP41).clone())
.build();
assert_matches!(sanitize(test).unwrap_err(), Error::Parse(err) => {
assert_matches!(dbg!(sanitize(test).unwrap_err()), Error::Parse(err) => {
assert_matches!(err.into_inner(), ParseError::UnsupportedFormat(MP41));
});
}
@ -756,7 +723,7 @@ mod test {
let test = test_mp4()
.ftyp(test_ftyp().major_brand(ISOM).compatible_brands(vec![]).clone())
.build();
assert_matches!(sanitize(test).unwrap_err(), Error::Parse(err) => {
assert_matches!(dbg!(sanitize(test).unwrap_err()), Error::Parse(err) => {
assert_matches!(err.into_inner(), ParseError::UnsupportedFormat(ISOM));
});
}
@ -764,7 +731,7 @@ mod test {
#[test]
fn no_trak() {
let test = test_mp4().moov(test_moov().trak(false).clone()).build();
assert_matches!(sanitize(test).unwrap_err(), Error::Parse(err) => {
assert_matches!(dbg!(sanitize(test).unwrap_err()), Error::Parse(err) => {
assert_matches!(err.into_inner(), ParseError::MissingRequiredBox(TRAK));
});
}
@ -775,7 +742,7 @@ mod test {
.boxes(&[FTYP, MDAT, MOOV][..])
.moov(test_moov().mdia(false).clone())
.build();
assert_matches!(sanitize(test).unwrap_err(), Error::Parse(err) => {
assert_matches!(dbg!(sanitize(test).unwrap_err()), Error::Parse(err) => {
assert_matches!(err.into_inner(), ParseError::MissingRequiredBox(MDIA));
});
}
@ -786,7 +753,7 @@ mod test {
.boxes(&[FTYP, MDAT, MOOV][..])
.moov(test_moov().minf(false).clone())
.build();
assert_matches!(sanitize(test).unwrap_err(), Error::Parse(err) => {
assert_matches!(dbg!(sanitize(test).unwrap_err()), Error::Parse(err) => {
assert_matches!(err.into_inner(), ParseError::MissingRequiredBox(MINF));
});
}
@ -797,7 +764,7 @@ mod test {
.boxes(&[FTYP, MDAT, MOOV][..])
.moov(test_moov().stbl(false).clone())
.build();
assert_matches!(sanitize(test).unwrap_err(), Error::Parse(err) => {
assert_matches!(dbg!(sanitize(test).unwrap_err()), Error::Parse(err) => {
assert_matches!(err.into_inner(), ParseError::MissingRequiredBox(STBL));
});
}
@ -808,7 +775,7 @@ mod test {
.boxes(&[FTYP, MDAT, MOOV][..])
.moov(test_moov().stco(false).clone())
.build();
assert_matches!(sanitize(test).unwrap_err(), Error::Parse(err) => {
assert_matches!(dbg!(sanitize(test).unwrap_err()), Error::Parse(err) => {
assert_matches!(err.into_inner(), ParseError::MissingRequiredBox(STCO | CO64));
});
}
@ -828,26 +795,8 @@ mod test {
.boxes(&[FTYP, MDAT, MOOV][..])
.moov(test_moov().co64(true).clone())
.build();
assert_matches!(sanitize(test).unwrap_err(), Error::Parse(err) => {
assert_matches!(dbg!(sanitize(test).unwrap_err()), Error::Parse(err) => {
assert_matches!(err.into_inner(), ParseError::InvalidBoxLayout);
});
}
#[test]
fn cumulative_mdat_box_size() {
let test_spec = test_mp4().mdat_data_until_eof().build_spec().unwrap();
let test_1 = test_spec.build();
let mdat_box_length = test_1.mdat.len as u32;
let config_bad = Config::builder().build();
assert_matches!(sanitize_with_config(test_1, config_bad).unwrap_err(), Error::Parse(err) => {
assert_matches!(err.into_inner(), ParseError::MissingRequiredBox(_));
});
let test_2 = test_spec.build();
let config_good = Config::builder()
.cumulative_mdat_box_size(Some(mdat_box_length))
.build();
test_2.sanitize_ok_with_config(config_good);
}
}

View File

@ -1,17 +1,30 @@
//! Unstable API for parsing individual MP4 box types.
#[macro_use]
mod macros;
mod array;
mod co64;
pub mod derive;
pub mod error;
mod ftyp;
mod hdlr;
mod header;
mod integers;
mod mdhd;
mod mdia;
mod minf;
mod moov;
mod mp4box;
mod mvhd;
mod stbl;
mod stco;
mod string;
mod stsc;
mod stsd;
mod stsz;
mod stts;
mod tkhd;
mod trak;
mod value;
@ -19,22 +32,36 @@ pub use array::{ArrayEntry, ArrayEntryMut, BoundedArray, UnboundedArray};
pub use co64::Co64Box;
pub use error::ParseError;
pub use ftyp::FtypBox;
pub use header::{box_type, fourcc, BoxHeader, BoxSize, BoxType, BoxUuid, ConstFullBoxHeader, FullBoxHeader};
pub use integers::Mp4Prim;
pub use mdia::MdiaBox;
pub use minf::MinfBox;
pub use moov::MoovBox;
pub use mp4box::{AnyMp4Box, BoxData, Boxes, BoxesValidator, Mp4Box, ParseBox, ParsedBox};
pub use stbl::{StblBox, StblCoMut};
pub use hdlr::HdlrBox;
pub use header::{box_type, fourcc, BoxFlags, BoxHeader, BoxSize, BoxType, BoxUuid, ConstFullBoxHeader, FullBoxHeader};
pub use integers::{
ConstI16, ConstI32, ConstI64, ConstI8, ConstU16, ConstU32, ConstU64, ConstU8, Mp4Prim, Mp4Transform,
};
pub use mdhd::MdhdBox;
pub use mdia::{MdiaBox, MdiaChildren, MdiaChildrenRef, MdiaChildrenRefMut};
pub use minf::{MinfBox, MinfChildren, MinfChildrenRef, MinfChildrenRefMut};
pub use moov::{MoovBox, MoovChildren, MoovChildrenRef, MoovChildrenRefMut};
pub use mp4box::{AnyMp4Box, BoundedBoxes, BoxData, Boxes, Mp4Box, ParseBox, ParseBoxes, ParsedBox};
pub use mvhd::MvhdBox;
pub use stbl::{StblBox, StblChildren, StblChildrenRef, StblChildrenRefMut, StblCo, StblCoRef, StblCoRefMut};
pub use stco::StcoBox;
pub use trak::TrakBox;
pub use string::Mp4String;
pub use stsc::{StscBox, StscEntry};
pub use stsd::StsdBox;
pub use stsz::StszBox;
pub use stts::{SttsBox, SttsEntry};
pub use tkhd::TkhdBox;
pub use trak::{TrakBox, TrakChildren, TrakChildrenRef, TrakChildrenRefMut};
pub use value::{Mp4Value, Mp4ValueReaderExt, Mp4ValueWriterExt};
pub use mediasan_common::parse::FourCC;
pub use mp4san_derive::{ParseBox, ParsedBox};
pub use mp4san_derive::{Mp4Prim, Mp4Value, ParseBox, ParseBoxes, ParsedBox};
#[cfg(test)]
mod test {
use std::fmt::Debug;
use assert_matches::assert_matches;
use bytes::BytesMut;
use super::*;
@ -66,12 +93,38 @@ mod test {
pub unbounded_array: UnboundedArray<u8>,
}
#[derive(Clone, Debug, ParseBox, ParsedBox)]
#[box_type = "vers"]
pub enum VersionedBox {
V0 {
_parsed_header: ConstFullBoxHeader,
foo: u32,
},
V1(ConstFullBoxHeader<1, 101>, u64),
V2(ConstFullBoxHeader<2, 102>),
}
#[derive(Clone, Debug, ParseBox, ParsedBox)]
#[box_type = "vers"]
pub enum StrictVersionedBox {
V0(ConstFullBoxHeader),
V1 { parsed_header: ConstFullBoxHeader<1> },
}
fn parse_box<T: ParseBox>(bytes: impl AsRef<[u8]>) -> T {
T::parse(&mut BytesMut::from(bytes.as_ref())).unwrap()
}
fn parse_box_err<T: ParseBox + Debug>(bytes: impl AsRef<[u8]>) -> ParseError {
T::parse(&mut BytesMut::from(bytes.as_ref())).unwrap_err().into_inner()
}
#[test]
fn test_size_simple() {
let not_a_real = NotARealBox { bar_ax: u64::MAX, foo_by: u32::MAX };
assert_eq!(
not_a_real.encoded_len(),
(<u64 as Mp4Prim>::encoded_len() + <u32 as Mp4Prim>::encoded_len()) as u64
(<u64 as Mp4Prim>::ENCODED_LEN + <u32 as Mp4Prim>::ENCODED_LEN) as u64
);
}
@ -82,16 +135,13 @@ mod test {
#[test]
fn test_type_bytes() {
assert_eq!(
NotARealBox::box_type(),
BoxType::FourCC(FourCC { value: *b"\xffX0\x00" })
);
assert_eq!(NotARealBox::NAME, BoxType::FourCC(FourCC { value: *b"\xffX0\x00" }));
}
#[test]
fn test_type_compact_int_decimal() {
assert_eq!(
AnotherFakeBox::box_type(),
AnotherFakeBox::NAME,
BoxType::FourCC(FourCC { value: 4283969538u32.to_be_bytes() })
);
}
@ -99,12 +149,53 @@ mod test {
#[test]
fn test_type_extended() {
let expected = BoxType::Uuid(BoxUuid { value: 0xc12fdd3f_1e93_464c_baee_7c4480628f58u128.to_be_bytes() });
assert_eq!(FakeUuidTypeBox::box_type(), expected);
assert_eq!(FakeUuidTypeBox::NAME, expected);
}
#[test]
fn test_type_compact_str() {
assert_eq!(Fifth::box_type(), BoxType::FourCC(FourCC { value: *b"xa04" }));
assert_eq!(Fifth::NAME, BoxType::FourCC(FourCC { value: *b"xa04" }));
}
#[test]
fn versioned() {
assert_matches!(
parse_box([0, 0, 0, 0, 0, 0, 0, 32]),
VersionedBox::V0 { _parsed_header: ConstFullBoxHeader, foo: 32 }
);
assert_matches!(
parse_box([1, 0, 0, 101, 0, 0, 0, 0, 0, 0, 0, 64]),
VersionedBox::V1(ConstFullBoxHeader, 64)
);
assert_matches!(parse_box([2, 0, 0, 102]), VersionedBox::V2(ConstFullBoxHeader));
assert_matches!(parse_box_err::<VersionedBox>([2, 0, 0, 0]), ParseError::InvalidInput);
assert_matches!(
parse_box_err::<VersionedBox>([3, 0, 0, 103, 104]),
ParseError::InvalidInput
);
}
#[test]
fn versioned_truncated() {
assert_matches!(parse_box_err::<VersionedBox>([]), ParseError::TruncatedBox);
assert_matches!(parse_box_err::<VersionedBox>([0, 0, 0, 0]), ParseError::TruncatedBox);
assert_matches!(
parse_box_err::<VersionedBox>([1, 0, 0, 101, 0, 0, 0, 0, 0, 0, 0]),
ParseError::TruncatedBox
);
}
#[test]
fn versioned_extra() {
assert_matches!(parse_box_err::<VersionedBox>([0; 9]), ParseError::InvalidInput);
assert_matches!(
parse_box_err::<VersionedBox>([1, 0, 0, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
ParseError::InvalidInput
);
assert_matches!(
parse_box_err::<VersionedBox>([2, 0, 0, 102, 1]),
ParseError::InvalidInput
);
}
#[test]

View File

@ -12,15 +12,15 @@ use crate::error::Result;
use super::{Mp4Prim, Mp4Value, Mp4ValueWriterExt, ParseError};
#[derive(Default, PartialEq, Eq)]
#[derive_where(Clone, Debug; C)]
#[derive(PartialEq, Eq)]
#[derive_where(Clone, Debug, Default; C)]
pub struct BoundedArray<C, T> {
entry_count: C,
array: UnboundedArray<T>,
}
#[derive(Default, PartialEq, Eq)]
#[derive_where(Clone, Debug)]
#[derive(PartialEq, Eq)]
#[derive_where(Clone, Debug, Default)]
pub struct UnboundedArray<T> {
entries: BytesMut,
_t: PhantomData<T>,
@ -59,7 +59,7 @@ impl<C: Clone, T: Mp4Prim> BoundedArray<C, T> {
impl<C: Mp4Prim + Into<u32> + Clone, T: Mp4Prim> Mp4Value for BoundedArray<C, T> {
fn parse(buf: &mut BytesMut) -> Result<Self, ParseError> {
let entry_count = C::parse(&mut *buf).while_parsing_type()?;
let entries_len = (T::encoded_len() as u32)
let entries_len = (T::ENCODED_LEN as u32)
.checked_mul(entry_count.clone().into())
.ok_or_else(|| report_attach!(ParseError::InvalidInput, "overflow", WhileParsingType::new::<Self>()))?;
ensure_attach!(
@ -73,7 +73,7 @@ impl<C: Mp4Prim + Into<u32> + Clone, T: Mp4Prim> Mp4Value for BoundedArray<C, T>
}
fn encoded_len(&self) -> u64 {
C::encoded_len() + self.array.encoded_len()
C::ENCODED_LEN + self.array.encoded_len()
}
fn put_buf<B: BufMut>(&self, mut buf: B) {
@ -96,18 +96,18 @@ impl<C: From<u32>, T: Mp4Prim> FromIterator<T> for BoundedArray<C, T> {
impl<T: Mp4Prim> UnboundedArray<T> {
pub fn entries(&self) -> impl Iterator<Item = ArrayEntry<'_, T>> + ExactSizeIterator + '_ {
self.entries
.chunks_exact(T::encoded_len() as usize)
.chunks_exact(T::ENCODED_LEN as usize)
.map(|data| ArrayEntry { data, _t: PhantomData })
}
pub fn entries_mut(&mut self) -> impl Iterator<Item = ArrayEntryMut<'_, T>> + ExactSizeIterator + '_ {
self.entries
.chunks_exact_mut(T::encoded_len() as usize)
.chunks_exact_mut(T::ENCODED_LEN as usize)
.map(|data| ArrayEntryMut { data, _t: PhantomData })
}
pub fn entry_count(&self) -> usize {
self.entries.len() / T::encoded_len() as usize
self.entries.len() / T::ENCODED_LEN as usize
}
}

View File

@ -1,6 +1,6 @@
#![allow(missing_docs)]
use super::{ArrayEntryMut, BoundedArray, ConstFullBoxHeader, ParseBox, ParsedBox};
use super::{ArrayEntry, ArrayEntryMut, BoundedArray, ConstFullBoxHeader, ParseBox, ParsedBox};
#[derive(Clone, Debug, Default, ParseBox, ParsedBox)]
#[box_type = "co64"]
@ -10,6 +10,10 @@ pub struct Co64Box {
}
impl Co64Box {
pub fn entries(&self) -> impl Iterator<Item = ArrayEntry<'_, u64>> + ExactSizeIterator + '_ {
self.entries.entries()
}
pub fn entries_mut(&mut self) -> impl Iterator<Item = ArrayEntryMut<'_, u64>> + ExactSizeIterator + '_ {
self.entries.entries_mut()
}

View File

@ -0,0 +1,3 @@
//! Unstable API used by the derive macros' generated code.
pub mod parse_boxes;

View File

@ -0,0 +1,185 @@
//! Unstable API used by the `#[derive(ParseBoxes)]` macro's generated code.
use std::iter;
use std::option;
use std::vec;
use derive_where::derive_where;
use nonempty::NonEmpty;
use crate::parse::ParseBox;
/// A type which can be used as a field in a `#[derive(ParseBoxes)]`.
pub trait Field {
/// The type of box that this field contains.
///
/// For example, for `Vec<T>`, this would be `T`.
type Type: ParseBox;
/// A type which can accumulate values of type `T` to construct a container like `Self`.
///
/// For example, for `Vec<U>`, this could be `Vec<T>`. For a plain `U`, this could be `Option<U>`.
type Accumulator<T>: Accumulator<T>;
/// The type returned by `<Self as Field>::into_iter`.
type IntoIter: Iterator<Item = Self::Type>;
/// Creates an [`Iterator`] of boxes contained in this field.
fn into_iter(self) -> Self::IntoIter;
}
/// A type which can accumulate values and unwrap into another type containing those values.
pub trait Accumulator<T> {
/// The type that this accumulator [unwraps](Self::unwrap) to.
type Unwrapped;
/// Appends an element to the accumulator.
fn push(&mut self, field: T);
/// Returns `true` if no more elements can be accumulated.
fn is_full(&self) -> bool;
/// Constructs a container from the values accumulated, if possible.
fn unwrap(self) -> Option<Self::Unwrapped>;
}
#[doc(hidden)]
#[derive(Clone)]
#[derive_where(Default)]
/// An [`Accumulator`] to build a [`T`].
pub struct PlainFieldAccumulator<T> {
value: Option<T>,
}
#[doc(hidden)]
#[derive(Clone)]
#[derive_where(Default)]
/// An [`Accumulator`] of `T`s to build a [`NonEmpty<T>`].
pub struct NonEmptyAccumulator<T> {
head: Option<T>,
tail: Vec<T>,
}
//
// Field impls
//
impl<T: ParseBox> Field for T {
type Type = Self;
type Accumulator<U> = PlainFieldAccumulator<U>;
type IntoIter = iter::Once<T>;
fn into_iter(self) -> Self::IntoIter {
iter::once(self)
}
}
impl<T: ParseBox> Field for Option<T> {
type Type = T;
type Accumulator<U> = Option<U>;
type IntoIter = option::IntoIter<T>;
fn into_iter(self) -> Self::IntoIter {
IntoIterator::into_iter(self)
}
}
impl<T: ParseBox> Field for Vec<T> {
type Type = T;
type Accumulator<U> = Vec<U>;
type IntoIter = vec::IntoIter<T>;
fn into_iter(self) -> Self::IntoIter {
IntoIterator::into_iter(self)
}
}
impl<T: ParseBox> Field for NonEmpty<T> {
type Type = T;
type Accumulator<U> = NonEmptyAccumulator<U>;
type IntoIter = <NonEmpty<T> as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
IntoIterator::into_iter(self)
}
}
//
// Accumulator impls
//
impl<T> Accumulator<T> for Option<T> {
type Unwrapped = Option<T>;
fn push(&mut self, field: T) {
*self = Some(field);
}
fn is_full(&self) -> bool {
self.is_some()
}
fn unwrap(self) -> Option<Self::Unwrapped> {
Some(self)
}
}
impl<T> Accumulator<T> for Vec<T> {
type Unwrapped = Self;
fn push(&mut self, field: T) {
self.push(field);
}
fn is_full(&self) -> bool {
false
}
fn unwrap(self) -> Option<Self::Unwrapped> {
Some(self)
}
}
//
// PlainFieldAccumulator impls
//
impl<T> Accumulator<T> for PlainFieldAccumulator<T> {
type Unwrapped = T;
fn push(&mut self, field: T) {
self.value = Some(field);
}
fn is_full(&self) -> bool {
self.value.is_some()
}
fn unwrap(self) -> Option<Self::Unwrapped> {
self.value
}
}
//
// NonEmptyAccumulator impls
//
impl<T> Accumulator<T> for NonEmptyAccumulator<T> {
type Unwrapped = NonEmpty<T>;
fn push(&mut self, field: T) {
if self.head.is_none() {
self.head = Some(field);
} else {
self.tail.push(field);
}
}
fn is_full(&self) -> bool {
false
}
fn unwrap(self) -> Option<Self::Unwrapped> {
self.head.map(|head| NonEmpty { head, tail: self.tail })
}
}

View File

@ -51,10 +51,12 @@ pub enum ParseError {
#[doc(hidden)]
/// Used by the derive macros' generated code.
pub trait __ParseResultExt: ResultExt + Sized {
#[track_caller]
fn while_parsing_box(self, box_type: BoxType) -> Self {
self.attach_printable(WhileParsingBox(box_type))
}
#[track_caller]
fn while_parsing_field<T>(self, box_type: BoxType, field_name: T) -> Self
where
T: Display + Debug + Send + Sync + 'static,
@ -62,10 +64,12 @@ pub trait __ParseResultExt: ResultExt + Sized {
self.attach_printable(WhileParsingField(box_type, field_name))
}
#[track_caller]
fn while_parsing_child(self, box_type: BoxType, child_box_type: BoxType) -> Self {
self.attach_printable(WhileParsingChild(box_type, child_box_type))
}
#[track_caller]
fn where_eq<T, U>(self, lhs: T, rhs: U) -> Self
where
T: Display + Debug + Send + Sync + 'static,
@ -77,23 +81,23 @@ pub trait __ParseResultExt: ResultExt + Sized {
pub(crate) use self::__ParseResultExt as ParseResultExt;
#[derive(Clone, Copy, Debug, Display)]
#[display("multiple `{}` boxes", _0)]
#[display(fmt = "multiple `{}` boxes", _0)]
pub(crate) struct MultipleBoxes(pub(crate) BoxType);
#[derive(Clone, Copy, Debug, Display)]
#[display("while parsing `{}` box", _0)]
#[display(fmt = "while parsing `{}` box", _0)]
pub(crate) struct WhileParsingBox(pub(crate) BoxType);
#[derive(Clone, Copy, Debug, Display)]
#[display("while parsing `{}` box field `{}`", _0, _1)]
#[display(fmt = "while parsing `{}` box field `{}`", _0, _1)]
pub(crate) struct WhileParsingField<T>(pub(crate) BoxType, pub(crate) T);
#[derive(Clone, Copy, Debug, Display)]
#[display("while parsing `{}` box child `{}`", _0, _1)]
#[display(fmt = "while parsing `{}` box child `{}`", _0, _1)]
pub(crate) struct WhileParsingChild(pub(crate) BoxType, pub(crate) BoxType);
#[derive(Clone, Copy, Debug, Display)]
#[display("where `{} = {}`", _0, _1)]
#[display(fmt = "where `{} = {}`", _0, _1)]
pub(crate) struct WhereEq<T, U>(pub(crate) T, pub(crate) U);
impl ReportableError for ParseError {

34
mp4san/src/parse/hdlr.rs Normal file
View File

@ -0,0 +1,34 @@
#![allow(missing_docs)]
use super::{ConstFullBoxHeader, ConstU32, FourCC, Mp4String, ParseBox, ParsedBox};
#[derive(Clone, Debug, ParseBox, ParsedBox)]
#[box_type = "hdlr"]
pub struct HdlrBox {
_parsed_header: ConstFullBoxHeader,
_pre_defined: ConstU32,
handler_type: FourCC,
_reserved: [ConstU32; 3],
name: Mp4String,
}
impl HdlrBox {
define_fourcc_lower!(VIDE, SOUN, HINT, AUXV, NULL);
}
#[cfg(test)]
mod test {
use super::*;
impl HdlrBox {
pub(crate) fn dummy() -> Self {
Self {
_parsed_header: Default::default(),
_pre_defined: Default::default(),
handler_type: Self::NULL,
_reserved: Default::default(),
name: Default::default(),
}
}
}
}

View File

@ -49,11 +49,15 @@ pub struct BoxUuid {
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct FullBoxHeader {
pub version: u8,
pub flags: u32,
pub flags: BoxFlags,
}
#[allow(missing_docs)]
#[derive(Clone, Copy, Debug, Default)]
#[derive(Clone, Copy, Debug, Default, From, PartialEq, Eq)]
pub struct BoxFlags(pub u32);
#[allow(missing_docs)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ConstFullBoxHeader<const VERSION: u8 = 0, const FLAGS: u32 = 0>;
#[allow(missing_docs)]
@ -131,11 +135,6 @@ impl BoxHeader {
Ok(Self { box_type: name, box_size: size })
}
pub fn overwrite_size(&mut self, actual_box_size: u32) {
assert_eq!(self.box_size, BoxSize::UntilEof);
self.box_size = BoxSize::Size(actual_box_size);
}
pub const fn encoded_len(&self) -> u64 {
let mut size = FourCC::size() + size_of::<u32>() as u64;
if let BoxSize::Ext(_) = self.box_size {
@ -277,7 +276,7 @@ impl fmt::Display for BoxUuid {
#[allow(missing_docs)]
impl FullBoxHeader {
pub const fn default() -> Self {
Self { version: 0, flags: 0 }
Self { version: 0, flags: BoxFlags(0) }
}
pub fn ensure_eq(&self, other: &Self) -> Result<(), ParseError> {
@ -289,48 +288,62 @@ impl FullBoxHeader {
ensure_attach!(
self.flags == other.flags,
ParseError::InvalidInput,
format!("box flags 0b{:024b} do not match 0b{:024b}", self.flags, other.flags),
format!(
"box flags 0b{:024b} do not match 0b{:024b}",
self.flags.0, other.flags.0
),
);
Ok(())
}
}
impl Mp4Prim for FullBoxHeader {
fn parse<B: Buf>(mut buf: B) -> Result<Self, ParseError> {
let version = u8::parse(&mut buf)?;
let flags = <[u8; 3]>::parse(buf)?;
let flags = u32::from_be_bytes([0, flags[0], flags[1], flags[2]]);
Ok(Self { version, flags })
}
const ENCODED_LEN: u64 = u8::ENCODED_LEN + BoxFlags::ENCODED_LEN;
fn encoded_len() -> u64 {
4
fn parse<B: Buf + AsRef<[u8]>>(mut buf: B) -> Result<Self, ParseError> {
ensure_attach!(buf.remaining() >= Self::ENCODED_LEN as usize, ParseError::TruncatedBox);
let version = u8::parse(&mut buf)?;
let flags = BoxFlags::parse(&mut buf)?;
Ok(Self { version, flags })
}
fn put_buf<B: BufMut>(&self, mut out: B) {
out.put_u8(self.version);
out.put_uint(self.flags.into(), 3);
self.flags.put_buf(out);
}
}
impl Mp4Prim for BoxFlags {
const ENCODED_LEN: u64 = 3;
fn parse<B: Buf + AsRef<[u8]>>(buf: B) -> Result<Self, ParseError> {
ensure_attach!(buf.remaining() >= Self::ENCODED_LEN as usize, ParseError::TruncatedBox);
let value = <[u8; 3]>::parse(buf)?;
let value = u32::from_be_bytes([0, value[0], value[1], value[2]]);
Ok(Self(value))
}
fn put_buf<B: BufMut>(&self, mut out: B) {
out.put_uint(self.0.into(), 3);
}
}
impl<const VERSION: u8, const FLAGS: u32> From<ConstFullBoxHeader<VERSION, FLAGS>> for FullBoxHeader {
fn from(_: ConstFullBoxHeader<VERSION, FLAGS>) -> Self {
Self { version: VERSION, flags: FLAGS }
Self { version: VERSION, flags: BoxFlags(FLAGS) }
}
}
impl<const VERSION: u8, const FLAGS: u32> Mp4Prim for ConstFullBoxHeader<VERSION, FLAGS> {
fn parse<B: Buf>(buf: B) -> Result<Self, ParseError> {
FullBoxHeader::parse(buf)?.ensure_eq(&Self.into())?;
const ENCODED_LEN: u64 = FullBoxHeader::ENCODED_LEN;
fn parse<B: Buf + AsRef<[u8]>>(mut buf: B) -> Result<Self, ParseError> {
FullBoxHeader::parse(buf.as_ref())?.ensure_eq(&Self.into())?;
buf.advance(Self::ENCODED_LEN as usize);
Ok(Self)
}
fn encoded_len() -> u64 {
FullBoxHeader::encoded_len()
}
fn put_buf<B: BufMut>(&self, mut out: B) {
out.put_u8(VERSION);
out.put_uint(FLAGS.into(), 3);
fn put_buf<B: BufMut>(&self, out: B) {
FullBoxHeader::from(*self).put_buf(out)
}
}

View File

@ -1,66 +1,157 @@
#![allow(missing_docs)]
use std::fmt;
use std::mem::size_of;
use std::num::{NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI8, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8};
use bytes::Buf;
use bytes::BufMut;
use bytes::{Buf, BufMut};
use fixed::traits::Fixed;
use fixed::types::{I16F16, I2F30};
use fixed::{FixedI16, FixedI32, FixedI64, FixedI8, FixedU16, FixedU32, FixedU64, FixedU8};
use mediasan_common::error::WhileParsingType;
use mediasan_common::ResultExt;
use nalgebra::{Matrix3, Matrix3x2, Vector3};
use crate::error::Result;
use super::error::WhereEq;
use super::{FourCC, Mp4ValueWriterExt, ParseError};
//
// types
//
pub trait Mp4Prim: Sized {
fn parse<B: Buf>(buf: B) -> Result<Self, ParseError>;
fn encoded_len() -> u64;
const ENCODED_LEN: u64;
fn parse<B: Buf + AsRef<[u8]>>(buf: B) -> Result<Self, ParseError>;
fn put_buf<B: BufMut>(&self, buf: B);
}
macro_rules! mp4_int {
($($ty:ty => ($get_fun:ident, $put_fun:ident)),+ $(,)?) => {
$(impl Mp4Prim for $ty {
fn parse<B: Buf>(mut buf: B) -> Result<Self, ParseError> {
if buf.remaining() < Self::encoded_len() as usize {
bail_attach!(ParseError::TruncatedBox, WhileParsingType::new::<$ty>());
}
Ok(buf.$get_fun())
}
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
pub struct ConstU8<const N: u8 = 0>;
fn encoded_len() -> u64 {
size_of::<Self>() as u64
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
pub struct ConstU16<const N: u16 = 0>;
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
pub struct ConstU32<const N: u32 = 0>;
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
pub struct ConstU64<const N: u64 = 0>;
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
pub struct ConstI8<const N: i8 = 0>;
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
pub struct ConstI16<const N: i16 = 0>;
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
pub struct ConstI32<const N: i32 = 0>;
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
pub struct ConstI64<const N: i64 = 0>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Mp4Transform {
pub transform: Matrix3x2<I16F16>,
pub normalizer: Vector3<I2F30>,
}
trait Mp4Fixed {}
//
// Mp4Prim impls
//
macro_rules! mp4_int {
($($ty:ty, $nonzero_ty:ty => ($get_fun:ident, $put_fun:ident)),+ $(,)?) => {
$(impl Mp4Prim for $ty {
const ENCODED_LEN: u64 = size_of::<Self>() as u64;
fn parse<B: Buf>(mut buf: B) -> Result<Self, ParseError> {
ensure_attach!(
buf.remaining() >= Self::ENCODED_LEN as usize,
ParseError::TruncatedBox,
WhileParsingType::new::<$ty>(),
);
Ok(buf.$get_fun())
}
fn put_buf<B: BufMut>(&self, mut buf: B) {
buf.$put_fun(*self)
}
})+
$(impl Mp4Prim for $nonzero_ty {
const ENCODED_LEN: u64 = size_of::<Self>() as u64;
fn parse<B: Buf>(mut buf: B) -> Result<Self, ParseError> {
ensure_attach!(
buf.remaining() >= Self::ENCODED_LEN as usize,
ParseError::TruncatedBox,
WhileParsingType::new::<$ty>(),
);
let Some(value) = <$nonzero_ty>::new(buf.$get_fun()) else {
bail_attach!(
ParseError::InvalidInput,
WhileParsingType::new::<$nonzero_ty>(),
);
};
Ok(value)
}
fn put_buf<B: BufMut>(&self, mut buf: B) {
buf.$put_fun(self.get())
}
})+
};
}
mp4_int! {
u8 => (get_u8, put_u8),
u16 => (get_u16, put_u16),
u32 => (get_u32, put_u32),
u64 => (get_u64, put_u64),
i8 => (get_i8, put_i8),
i16 => (get_i16, put_i16),
i32 => (get_i32, put_i32),
i64 => (get_i64, put_i64),
u8, NonZeroU8 => (get_u8, put_u8),
u16, NonZeroU16 => (get_u16, put_u16),
u32, NonZeroU32 => (get_u32, put_u32),
u64, NonZeroU64 => (get_u64, put_u64),
i8, NonZeroI8 => (get_i8, put_i8),
i16, NonZeroI16 => (get_i16, put_i16),
i32, NonZeroI32 => (get_i32, put_i32),
i64, NonZeroI64 => (get_i64, put_i64),
}
impl<T: Mp4Prim, const N: usize> Mp4Prim for [T; N] {
fn parse<B: Buf>(mut buf: B) -> Result<Self, ParseError> {
impl<T: Fixed + Mp4Fixed> Mp4Prim for T
where
T::Bits: Mp4Prim,
{
const ENCODED_LEN: u64 = <T::Bits>::ENCODED_LEN;
fn parse<B: Buf + AsRef<[u8]>>(buf: B) -> Result<Self, ParseError> {
let bits = <T::Bits>::parse(buf)?;
Ok(T::from_bits(bits))
}
fn put_buf<B: BufMut>(&self, buf: B) {
self.to_bits().put_buf(buf)
}
}
impl<T: Mp4Prim, const N: usize> Mp4Prim for [T; N]
where
[T; N]: Default,
{
const ENCODED_LEN: u64 = T::ENCODED_LEN * N as u64;
fn parse<B: Buf + AsRef<[u8]>>(mut buf: B) -> Result<Self, ParseError> {
ensure_attach!(
buf.remaining() >= Self::encoded_len() as usize,
buf.remaining() >= Self::ENCODED_LEN as usize,
ParseError::TruncatedBox,
WhileParsingType::new::<Self>(),
);
Ok([(); N].map(|()| T::parse(&mut buf).unwrap_or_else(|_| unreachable!())))
}
fn encoded_len() -> u64 {
size_of::<Self>() as u64
let mut parsed: [T; N] = Default::default();
for value in &mut parsed {
*value = T::parse(&mut buf)?;
}
Ok(parsed)
}
fn put_buf<B: BufMut>(&self, mut buf: B) {
@ -71,15 +162,124 @@ impl<T: Mp4Prim, const N: usize> Mp4Prim for [T; N] {
}
impl Mp4Prim for FourCC {
fn parse<B: Buf>(buf: B) -> Result<Self, ParseError> {
fn parse<B: Buf + AsRef<[u8]>>(buf: B) -> Result<Self, ParseError> {
Mp4Prim::parse(buf).map(|value| Self { value }).while_parsing_type()
}
fn encoded_len() -> u64 {
Self::size()
}
const ENCODED_LEN: u64 = Self::size();
fn put_buf<B: BufMut>(&self, mut buf: B) {
buf.put_mp4_value(&self.value);
}
}
//
// Const* impls
//
macro_rules! mp4_const_int {
($($ident:ident => $ty:ty),+ $(,)?) => {
$(
impl<const N: $ty> Mp4Prim for $ident<N> {
const ENCODED_LEN: u64 = <$ty>::ENCODED_LEN;
fn parse<B: Buf + AsRef<[u8]>>(mut buf: B) -> Result<Self, ParseError> {
ensure_attach!(
<$ty>::parse(buf.as_ref())? == N,
ParseError::InvalidInput,
WhereEq(stringify!(N), N)
);
buf.advance(Self::ENCODED_LEN as usize);
Ok(Self)
}
fn put_buf<B: BufMut>(&self, buf: B) {
N.put_buf(buf)
}
}
impl<const N: $ty> fmt::Debug for $ident<N> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct(&format!(concat!(stringify!($ident), "<{}>"), N))
.finish()
}
}
)+
};
}
mp4_const_int! {
ConstU8 => u8,
ConstU16 => u16,
ConstU32 => u32,
ConstU64 => u64,
ConstI8 => i8,
ConstI16 => i16,
ConstI32 => i32,
ConstI64 => i64,
}
//
// Mp4Transform impls
//
impl Mp4Transform {
pub const UNITY: Self = Self {
transform: Matrix3x2::new(
I16F16::ONE,
I16F16::ZERO,
I16F16::ZERO,
I16F16::ZERO,
I16F16::ONE,
I16F16::ZERO,
),
normalizer: Vector3::new(I2F30::ZERO, I2F30::ZERO, I2F30::ONE),
};
}
impl Default for Mp4Transform {
fn default() -> Self {
Self::UNITY
}
}
impl Mp4Prim for Mp4Transform {
const ENCODED_LEN: u64 = <[i32; 9]>::ENCODED_LEN;
fn parse<B: Buf + AsRef<[u8]>>(mut buf: B) -> Result<Self, ParseError> {
let mut raw = Matrix3::default();
for value in &mut raw {
*value = Mp4Prim::parse(&mut buf)?;
}
Ok(Self {
transform: raw.fixed_columns::<2>(0).map(I16F16::from_bits),
normalizer: raw.column(2).map(I2F30::from_bits),
})
}
fn put_buf<B: BufMut>(&self, mut buf: B) {
let raw = Matrix3::from_columns(&[
self.transform.column(0).map(I16F16::to_bits),
self.transform.column(1).map(I16F16::to_bits),
self.normalizer.map(I2F30::to_bits),
]);
for row in raw.row_iter() {
for value in &row {
buf.put_mp4_value(value);
}
}
}
}
//
// Mp4Fixed impls
//
impl<Frac> Mp4Fixed for FixedI8<Frac> {}
impl<Frac> Mp4Fixed for FixedI16<Frac> {}
impl<Frac> Mp4Fixed for FixedI32<Frac> {}
impl<Frac> Mp4Fixed for FixedI64<Frac> {}
impl<Frac> Mp4Fixed for FixedU8<Frac> {}
impl<Frac> Mp4Fixed for FixedU16<Frac> {}
impl<Frac> Mp4Fixed for FixedU32<Frac> {}
impl<Frac> Mp4Fixed for FixedU64<Frac> {}

View File

@ -0,0 +1,10 @@
macro_rules! define_fourcc_lower {
($($name:ident),+ $(,)?) => {
paste::paste! {
$(
#[doc = concat!("The `", stringify!([<$name:lower>]), "` FourCC code.")]
pub const $name: $crate::parse::FourCC = $crate::parse::FourCC::from_str(stringify!([<$name:lower>]));
)+
}
};
}

44
mp4san/src/parse/mdhd.rs Normal file
View File

@ -0,0 +1,44 @@
#![allow(missing_docs)]
use super::{ConstFullBoxHeader, ConstU16, ParseBox, ParsedBox};
#[derive(Clone, Debug, ParseBox, ParsedBox)]
#[box_type = "mdhd"]
pub enum MdhdBox {
V1 {
_parsed_header: ConstFullBoxHeader<1>,
creation_time: u64,
modification_time: u64,
timescale: u32,
duration: u64,
languages: [u8; 2],
_pre_defined: ConstU16,
},
V0 {
_parsed_header: ConstFullBoxHeader,
creation_time: u32,
modification_time: u32,
timescale: u32,
duration: u32,
languages: [u8; 2],
_pre_defined: ConstU16,
},
}
impl MdhdBox {
pub const DURATION_UNDETERMINED_V0: u32 = u32::MAX;
pub const DURATION_UNDETERMINED_V1: u64 = u64::MAX;
#[cfg(test)]
pub(crate) fn dummy() -> Self {
Self::V1 {
_parsed_header: ConstFullBoxHeader,
creation_time: 0,
modification_time: 0,
timescale: 0,
duration: Self::DURATION_UNDETERMINED_V1,
languages: [0; 2],
_pre_defined: Default::default(),
}
}
}

View File

@ -1,26 +1,44 @@
#![allow(missing_docs)]
use derive_more::{Deref, DerefMut};
use crate::error::Result;
use super::error::ParseResultExt;
use super::mp4box::Boxes;
use super::{BoxType, MinfBox, ParseBox, ParseError, ParsedBox};
use super::{HdlrBox, MdhdBox, MinfBox, ParseBox, ParseBoxes, ParseError, ParsedBox};
#[derive(Clone, Debug, ParseBox, ParsedBox)]
#[derive(Clone, Debug, Deref, DerefMut, ParseBox, ParsedBox)]
#[box_type = "mdia"]
pub struct MdiaBox {
pub children: Boxes,
pub children: Boxes<MdiaChildren>,
}
const NAME: BoxType = BoxType::MDIA;
#[non_exhaustive]
#[derive(Clone, Debug, ParseBoxes)]
#[box_type = "mdia"]
pub struct MdiaChildren {
pub header: MdhdBox,
pub handler: HdlrBox,
pub info: MinfBox,
}
impl MdiaBox {
#[cfg(test)]
pub(crate) fn with_children<C: Into<Boxes>>(children: C) -> Self {
Self { children: children.into() }
}
pub fn minf_mut(&mut self) -> Result<&mut MinfBox, ParseError> {
self.children.get_one_mut().while_parsing_child(NAME, BoxType::MINF)
pub fn with_children(children: MdiaChildren) -> Result<Self, ParseError> {
Ok(Self { children: Boxes::new(children, [])? })
}
}
#[cfg(test)]
mod test {
use super::*;
impl MdiaBox {
pub(crate) fn dummy() -> Self {
Self::new(MdhdBox::dummy(), HdlrBox::dummy(), MinfBox::dummy()).unwrap()
}
pub(crate) fn new(header: MdhdBox, handler: HdlrBox, info: MinfBox) -> Result<Self, ParseError> {
Self::with_children(MdiaChildren { header, handler, info })
}
}
}

View File

@ -1,26 +1,43 @@
#![allow(missing_docs)]
use derive_more::{Deref, DerefMut};
use crate::error::Result;
use super::error::ParseResultExt;
use super::mp4box::Boxes;
use super::{BoxType, ParseBox, ParseError, ParsedBox, StblBox};
use super::{ParseBox, ParseBoxes, ParseError, ParsedBox, StblBox};
#[derive(Clone, Debug, ParseBox, ParsedBox)]
#[derive(Clone, Debug, Deref, DerefMut, ParseBox, ParsedBox)]
#[box_type = "minf"]
pub struct MinfBox {
children: Boxes,
pub children: Boxes<MinfChildren>,
}
const NAME: BoxType = BoxType::MINF;
#[non_exhaustive]
#[derive(Clone, Debug, ParseBoxes)]
#[box_type = "minf"]
pub struct MinfChildren {
pub samples: StblBox,
}
impl MinfBox {
#[cfg(test)]
pub(crate) fn with_children<C: Into<Boxes>>(children: C) -> Self {
Self { children: children.into() }
}
pub fn stbl_mut(&mut self) -> Result<&mut StblBox, ParseError> {
self.children.get_one_mut().while_parsing_child(NAME, BoxType::STBL)
pub fn with_children(children: MinfChildren) -> Result<Self, ParseError> {
Ok(Self { children: Boxes::new(children, [])? })
}
}
#[cfg(test)]
mod test {
use super::*;
impl MinfBox {
pub(crate) fn dummy() -> Self {
Self::new(StblBox::dummy()).unwrap()
}
#[cfg(test)]
pub(crate) fn new(samples: StblBox) -> Result<Self, ParseError> {
Self::with_children(MinfChildren { samples })
}
}
}

View File

@ -1,68 +1,57 @@
#![allow(missing_docs)]
use derive_more::{Deref, DerefMut};
use nonempty::NonEmpty;
use crate::error::Result;
use super::error::{ParseResultExt, WhileParsingField};
use super::{BoxType, Boxes, BoxesValidator, ParseBox, ParseError, ParsedBox, TrakBox};
use super::{Boxes, MvhdBox, ParseBox, ParseBoxes, ParseError, ParsedBox, TrakBox};
#[derive(Clone, Debug, ParseBox, ParsedBox)]
#[derive(Clone, Debug, Deref, DerefMut, ParseBox, ParsedBox)]
#[box_type = "moov"]
pub struct MoovBox {
children: Boxes<MoovChildrenValidator>,
pub children: Boxes<MoovChildren>,
}
pub(crate) struct MoovChildrenValidator;
const NAME: BoxType = BoxType::MOOV;
#[non_exhaustive]
#[derive(Clone, Debug, ParseBoxes)]
#[box_type = "moov"]
pub struct MoovChildren {
pub header: MvhdBox,
pub tracks: NonEmpty<TrakBox>,
}
impl MoovBox {
#[cfg(test)]
pub(crate) fn with_children<C: Into<Boxes<MoovChildrenValidator>>>(children: C) -> Self {
Self { children: children.into() }
}
pub fn traks(&mut self) -> impl Iterator<Item = Result<&mut TrakBox, ParseError>> + '_ {
self.children
.get_mut()
.map(|result| result.while_parsing_child(NAME, BoxType::TRAK))
}
}
impl BoxesValidator for MoovChildrenValidator {
fn validate<V>(children: &Boxes<V>) -> Result<(), ParseError> {
ensure_attach!(
children.box_types().any(|box_type| box_type == BoxType::TRAK),
ParseError::MissingRequiredBox(BoxType::TRAK),
WhileParsingField(NAME, "children"),
);
Ok(())
pub fn with_children(children: MoovChildren) -> Result<Self, ParseError> {
Ok(Self { children: Boxes::new(children, [])? })
}
}
#[cfg(test)]
mod test {
use bytes::BytesMut;
use crate::parse::Mp4Box;
use crate::parse::{BoxType, Mp4Value};
use crate::util::test::test_mvhd;
use super::*;
fn test_trak() -> Mp4Box<TrakBox> {
Mp4Box::with_data(TrakBox::with_children(vec![]).into()).unwrap()
impl MoovBox {
pub(crate) fn dummy() -> Self {
Self::new(MvhdBox::dummy(), NonEmpty::new(TrakBox::dummy())).unwrap()
}
pub(crate) fn new(header: MvhdBox, tracks: NonEmpty<TrakBox>) -> Result<Self, ParseError> {
Self::with_children(MoovChildren { header, tracks })
}
}
#[test]
fn roundtrip() {
let mut data = BytesMut::new();
MoovBox::with_children(vec![test_trak().into()]).put_buf(&mut data);
MoovBox::parse(&mut data).unwrap();
MoovBox::parse(&mut MoovBox::dummy().to_bytes()).unwrap();
}
#[test]
fn no_traks() {
let mut data = BytesMut::new();
MoovBox::with_children(vec![]).put_buf(&mut data);
let err = MoovBox::parse(&mut data).unwrap_err();
let err = MoovBox::parse(&mut test_mvhd().to_bytes()).unwrap_err();
assert!(
matches!(err.get_ref(), ParseError::MissingRequiredBox(BoxType::TRAK)),
"{err}",

View File

@ -1,27 +1,28 @@
#![allow(missing_docs)]
use std::fmt::Debug;
use std::iter;
use std::marker::PhantomData;
use std::mem::take;
use std::pin::Pin;
use std::result::Result as StdResult;
use bytes::{Buf, BufMut, BytesMut};
use derive_more::From;
use derive_more::{Deref, DerefMut, From};
use derive_where::derive_where;
use downcast_rs::{impl_downcast, Downcast};
use dyn_clonable::clonable;
use futures_util::io::BufReader;
use futures_util::{AsyncRead, AsyncReadExt};
use mediasan_common::error::WhileParsingType;
use mediasan_common::{AsyncSkipExt, ResultExt};
use crate::error::Result;
use crate::error::{Report, Result};
use crate::parse::error::ParseResultExt;
use crate::util::IoResultExt;
use crate::{AsyncSkip, BoxDataTooLarge, Error};
use super::error::{MultipleBoxes, WhileParsingBox};
use super::{BoxHeader, BoxType, Mp4Value, ParseError};
use super::{BoxHeader, BoxType, Mp4Prim, Mp4Value, Mp4ValueWriterExt, ParseError};
#[derive(Debug)]
#[derive_where(Clone; BoxData<T>)]
@ -40,9 +41,9 @@ pub enum BoxData<T: ?Sized> {
}
pub trait ParseBox: Sized {
fn parse(buf: &mut BytesMut) -> Result<Self, ParseError>;
const NAME: BoxType;
fn box_type() -> BoxType;
fn parse(buf: &mut BytesMut) -> Result<Self, ParseError>;
}
#[clonable]
@ -56,30 +57,67 @@ pub trait ParsedBox: Clone + Debug + Downcast {
#[derive_where(Clone, Debug, Default)]
pub struct Boxes<V = ()> {
boxes: Vec<AnyMp4Box>,
_validator: PhantomData<V>,
_typed: PhantomData<V>,
}
pub trait BoxesValidator {
fn validate<V>(_boxes: &Boxes<V>) -> Result<(), ParseError> {
Ok(())
}
#[derive(Deref, DerefMut)]
#[derive_where(Clone, Debug, Default; C)]
pub struct BoundedBoxes<C, V = ()> {
#[deref]
#[deref_mut]
boxes: Boxes<V>,
_count: PhantomData<C>,
}
pub trait ParseBoxes: Sized {
type Ref<'a>
where
Self: 'a;
type RefMut<'a>
where
Self: 'a;
type IntoIter: Iterator<Item = AnyMp4Box>;
fn validate(boxes: &mut [AnyMp4Box]) -> Result<(), ParseError> {
Self::parse(boxes).map(drop)
}
fn parse<'a>(boxes: &'a mut [AnyMp4Box]) -> Result<Self::RefMut<'a>, ParseError>
where
Self: 'a;
fn parsed<'a>(boxes: &'a [AnyMp4Box]) -> Self::Ref<'a>
where
Self: 'a;
fn try_into_iter(self) -> Result<Self::IntoIter, ParseError>;
}
//
// Mp4Box impls
//
impl<T: ParsedBox + ?Sized> Mp4Box<T> {
pub fn with_data(data: BoxData<T>) -> Result<Self, ParseError>
where
T: ParseBox,
{
let parsed_header = BoxHeader::with_data_size(T::box_type(), data.encoded_len())?;
let parsed_header = BoxHeader::with_data_size(T::NAME, data.encoded_len())?;
Ok(Self { parsed_header, data })
}
pub fn with_parsed(data: Box<T>) -> Result<Self, ParseError>
where
T: ParseBox,
{
Self::with_data(BoxData::from(data))
}
/// Read and parse a box's data assuming its header has already been read.
pub(crate) async fn read_data<R>(
mut reader: Pin<&mut BufReader<R>>,
header: BoxHeader,
max_size: u64,
) -> StdResult<Self, Error>
///
/// It is highly recommended that `R` be a buffered reader.
pub async fn read_data<R>(mut reader: Pin<&mut R>, header: BoxHeader, max_size: u64) -> StdResult<Self, Error>
where
R: AsyncRead + AsyncSkip,
T: ParseBox,
@ -117,8 +155,12 @@ impl<T: ParsedBox + ?Sized> Mp4Box<T> {
}
}
pub fn box_type(&self) -> BoxType {
self.parsed_header.box_type()
}
pub fn parse_data_as<U: ParseBox + ParsedBox + Into<Box<T>>>(&mut self) -> Result<Option<&mut U>, ParseError> {
if self.parsed_header.box_type() != U::box_type() {
if self.parsed_header.box_type() != U::NAME {
return Ok(None);
}
self.data.parse_as()
@ -127,8 +169,8 @@ impl<T: ParsedBox + ?Sized> Mp4Box<T> {
impl<T: ParsedBox + ?Sized> Mp4Value for Mp4Box<T> {
fn parse(mut buf: &mut BytesMut) -> Result<Self, ParseError> {
let parsed_header = BoxHeader::parse(&mut buf).attach_printable(WhileParsingType::new::<Self>())?;
let data = BoxData::get_from_bytes_mut(buf, &parsed_header).attach_printable(WhileParsingType::new::<Self>())?;
let parsed_header = BoxHeader::parse(&mut buf).attach_printable(WhileParsingType::new::<T>())?;
let data = BoxData::get_from_bytes_mut(buf, &parsed_header).attach_printable(WhileParsingType::new::<T>())?;
Ok(Self { parsed_header, data })
}
@ -161,7 +203,7 @@ impl<T: ParsedBox> From<Mp4Box<T>> for AnyMp4Box {
impl<T: ParsedBox + ?Sized> BoxData<T> {
pub fn get_from_bytes_mut(buf: &mut BytesMut, header: &BoxHeader) -> Result<Self, ParseError> {
match header.box_data_size()? {
match header.box_data_size().while_parsing_box(header.box_type())? {
None => Ok(Self::Bytes(take(buf))),
Some(box_data_size) => match box_data_size.try_into() {
Ok(box_data_size) => {
@ -186,12 +228,12 @@ impl<T: ParsedBox + ?Sized> BoxData<T> {
T: ParseBox + Sized,
{
if let BoxData::Bytes(data) = self {
let parsed = T::parse(data).while_parsing_type()?;
let parsed = T::parse(data).while_parsing_box(T::NAME)?;
ensure_attach!(
data.is_empty(),
ParseError::InvalidInput,
"extra unparsed data",
WhileParsingBox(T::box_type()),
WhileParsingBox(T::NAME),
);
*self = Self::Parsed(Box::new(parsed));
}
@ -201,14 +243,21 @@ impl<T: ParsedBox + ?Sized> BoxData<T> {
}
}
pub fn parsed<U: ParsedBox>(&self) -> Option<&U> {
let BoxData::Parsed(parsed) = self else { return None };
// This is subtle because of the generic T; we have to make sure Downcast::as_any_mut is called on T and
// not on Box<T>, or the subsequent Any::downcast_ref::<U>() will fail.
<T>::as_any(parsed).downcast_ref()
}
fn parse_as<U: ParseBox + ParsedBox + Into<Box<T>>>(&mut self) -> Result<Option<&mut U>, ParseError> {
if let BoxData::Bytes(data) = self {
let parsed = U::parse(data).while_parsing_type()?;
let parsed = U::parse(data).while_parsing_box(U::NAME)?;
ensure_attach!(
data.is_empty(),
ParseError::InvalidInput,
"extra unparsed data",
WhileParsingBox(U::box_type()),
WhileParsingBox(U::NAME),
);
*self = Self::Parsed(parsed.into());
}
@ -268,6 +317,21 @@ impl<'a, T: ParsedBox + 'a> From<T> for Box<dyn ParsedBox + 'a> {
// Boxes impls
//
impl<V: ParseBoxes> Boxes<V> {
pub fn new(typed: V, untyped: impl IntoIterator<Item = AnyMp4Box>) -> Result<Self, ParseError> {
let boxes = typed.try_into_iter()?.chain(untyped).collect();
Ok(Self { boxes, _typed: PhantomData })
}
pub fn parsed(&self) -> V::Ref<'_> {
V::parsed(&self.boxes)
}
pub fn parsed_mut(&mut self) -> V::RefMut<'_> {
V::parse(&mut self.boxes).unwrap_or_else(|_| unreachable!())
}
}
impl<V> Boxes<V> {
pub fn box_types(&self) -> impl Iterator<Item = BoxType> + ExactSizeIterator + '_ {
self.boxes.iter().map(|mp4box| mp4box.parsed_header.box_type())
@ -281,24 +345,40 @@ impl<V> Boxes<V> {
pub fn get_one_mut<T: ParseBox + ParsedBox>(&mut self) -> Result<&mut T, ParseError> {
ensure_attach!(
self.box_types().filter(|box_type| *box_type == T::box_type()).count() <= 1,
self.box_types().filter(|box_type| *box_type == T::NAME).count() <= 1,
ParseError::InvalidBoxLayout,
MultipleBoxes(T::box_type()),
MultipleBoxes(T::NAME),
);
self.get_mut()
.next()
.ok_or_else(|| ParseError::MissingRequiredBox(T::box_type()))?
.ok_or_else(|| ParseError::MissingRequiredBox(T::NAME))?
}
pub fn is_empty(&self) -> bool {
self.boxes.is_empty()
}
pub fn len(&self) -> usize {
self.boxes.len()
}
pub fn iter(&self) -> impl Iterator<Item = &AnyMp4Box> + '_ {
self.boxes.iter()
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut AnyMp4Box> + '_ {
self.boxes.iter_mut()
}
}
impl<V: BoxesValidator> Mp4Value for Boxes<V> {
impl<V: ParseBoxes> Mp4Value for Boxes<V> {
fn parse(buf: &mut BytesMut) -> Result<Self, ParseError> {
let mut boxes = Vec::new();
while buf.has_remaining() {
boxes.push(Mp4Box::parse(buf)?);
}
let boxes = Self { boxes, _validator: PhantomData };
V::validate(&boxes)?;
V::validate(&mut boxes)?;
let boxes = Self { boxes, _typed: PhantomData };
Ok(boxes)
}
@ -313,14 +393,88 @@ impl<V: BoxesValidator> Mp4Value for Boxes<V> {
}
}
impl<V> From<Vec<AnyMp4Box>> for Boxes<V> {
fn from(boxes: Vec<AnyMp4Box>) -> Self {
Self { boxes, _validator: PhantomData }
impl<V: ParseBoxes> TryFrom<Vec<AnyMp4Box>> for Boxes<V> {
type Error = Report<ParseError>;
fn try_from(mut boxes: Vec<AnyMp4Box>) -> Result<Self, ParseError> {
V::validate(&mut boxes)?;
Ok(Self { boxes, _typed: PhantomData })
}
}
impl<'a, V> IntoIterator for &'a Boxes<V> {
type Item = &'a AnyMp4Box;
// TODO when stabilizing the API, replace this with a wrapper.
type IntoIter = std::slice::Iter<'a, AnyMp4Box>;
fn into_iter(self) -> Self::IntoIter {
self.boxes.iter()
}
}
impl<'a, V> IntoIterator for &'a mut Boxes<V> {
type Item = &'a mut AnyMp4Box;
// TODO when stabilizing the API, replace this with a wrapper.
type IntoIter = std::slice::IterMut<'a, AnyMp4Box>;
fn into_iter(self) -> Self::IntoIter {
self.boxes.iter_mut()
}
}
//
// BoxesValidator impls
// BoundedBoxes impls
//
impl BoxesValidator for () {}
impl<C, V: ParseBoxes> From<Boxes<V>> for BoundedBoxes<C, V> {
fn from(boxes: Boxes<V>) -> Self {
Self { boxes, _count: PhantomData }
}
}
impl<C: Mp4Prim + Into<u32> + TryFrom<u32>, V: ParseBoxes> Mp4Value for BoundedBoxes<C, V> {
fn parse(buf: &mut BytesMut) -> Result<Self, ParseError> {
let count = C::parse(&mut *buf)?;
let boxes = Boxes::parse(&mut *buf)?;
ensure_attach!(boxes.len() as u32 == count.into(), ParseError::InvalidInput);
Ok(Self { boxes, _count: PhantomData })
}
fn encoded_len(&self) -> u64 {
C::ENCODED_LEN + self.boxes.encoded_len()
}
fn put_buf<B: BufMut>(&self, mut buf: B) {
buf.put_mp4_value(&C::try_from(self.boxes.len() as u32).unwrap_or_else(|_| panic!()));
self.boxes.put_buf(buf);
}
}
//
// ParseBoxes impls
//
impl ParseBoxes for () {
type Ref<'a> = ();
type RefMut<'a> = ();
type IntoIter = iter::Empty<AnyMp4Box>;
fn parse<'a>(_boxes: &'a mut [AnyMp4Box]) -> Result<Self, ParseError>
where
Self: 'a,
{
Ok(())
}
fn parsed<'a>(_boxes: &'a [AnyMp4Box]) -> Self::Ref<'a>
where
Self: 'a,
{
}
fn try_into_iter(self) -> Result<Self::IntoIter, ParseError> {
Ok(iter::empty())
}
}

75
mp4san/src/parse/mvhd.rs Normal file
View File

@ -0,0 +1,75 @@
#![allow(missing_docs)]
use std::num::NonZeroU32;
use fixed::types::{I16F16, I8F8};
use super::{ConstFullBoxHeader, ConstU16, ConstU32, Mp4Transform, ParseBox, ParsedBox};
#[derive(Clone, Debug, ParseBox, ParsedBox)]
#[box_type = "mvhd"]
pub enum MvhdBox {
V1 {
_parsed_header: ConstFullBoxHeader<1>,
creation_time: u64,
modification_time: u64,
timescale: u32,
duration: u64,
rate: I16F16,
volume: I8F8,
_reserved: ConstU16,
_reserved_2: [ConstU32; 2],
matrix: Mp4Transform,
_pre_defined: [ConstU32; 6],
next_track_id: NonZeroU32,
},
V0 {
_parsed_header: ConstFullBoxHeader,
creation_time: u32,
modification_time: u32,
timescale: u32,
duration: u32,
rate: I16F16,
volume: I8F8,
_reserved: ConstU16,
_reserved_2: [ConstU32; 2],
matrix: Mp4Transform,
_pre_defined: [ConstU32; 6],
next_track_id: NonZeroU32,
},
}
impl MvhdBox {
pub const DURATION_UNDETERMINED_V0: u32 = u32::MAX;
pub const DURATION_UNDETERMINED_V1: u64 = u64::MAX;
pub const NEXT_TRACK_ID_UNDETERMINED: NonZeroU32 = {
let Some(value) = NonZeroU32::new(u32::MAX) else {
unreachable!()
};
value
};
}
#[cfg(test)]
mod test {
use super::*;
impl MvhdBox {
pub(crate) fn dummy() -> Self {
Self::V1 {
_parsed_header: Default::default(),
creation_time: 0,
modification_time: 0,
timescale: 0,
duration: Self::DURATION_UNDETERMINED_V1,
rate: 1i16.into(),
volume: 1.into(),
_reserved: Default::default(),
_reserved_2: Default::default(),
matrix: Default::default(),
_pre_defined: Default::default(),
next_track_id: Self::NEXT_TRACK_ID_UNDETERMINED,
}
}
}
}

View File

@ -1,51 +1,180 @@
#![allow(missing_docs)]
use std::vec;
use derive_more::{Deref, DerefMut};
use crate::error::Result;
use crate::parse::error::WhileParsingBox;
use super::error::{ParseResultExt, WhileParsingChild};
use super::{BoxType, Boxes, Co64Box, ParseBox, ParseError, ParsedBox, StcoBox};
use super::{
AnyMp4Box, Boxes, Co64Box, ParseBox, ParseBoxes, ParseError, ParsedBox, StcoBox, StscBox, StsdBox, StszBox, SttsBox,
};
#[derive(Clone, Debug, ParseBox, ParsedBox)]
#[derive(Clone, Debug, Deref, DerefMut, ParseBox, ParsedBox)]
#[box_type = "stbl"]
pub struct StblBox {
children: Boxes,
pub children: Boxes<StblChildren>,
}
#[non_exhaustive]
#[derive(Clone, Debug)]
pub struct StblChildren {
pub sample_descriptions: StsdBox,
pub time_to_sample: SttsBox,
pub sample_to_chunk: StscBox,
pub sample_sizes: StszBox,
pub chunk_offsets: StblCo,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug)]
pub struct StblChildrenRef<'a> {
pub sample_descriptions: &'a StsdBox,
pub time_to_sample: &'a SttsBox,
pub sample_to_chunk: &'a StscBox,
pub sample_sizes: &'a StszBox,
pub chunk_offsets: StblCoRef<'a>,
}
#[non_exhaustive]
#[derive(Debug)]
pub struct StblChildrenRefMut<'a> {
pub sample_descriptions: &'a mut StsdBox,
pub time_to_sample: &'a mut SttsBox,
pub sample_to_chunk: &'a mut StscBox,
pub sample_sizes: &'a mut StszBox,
pub chunk_offsets: StblCoRefMut<'a>,
}
#[derive(Clone, Debug)]
pub enum StblCo {
Stco(StcoBox),
Co64(Co64Box),
}
#[derive(Clone, Copy, Debug)]
pub enum StblCoRef<'a> {
Stco(&'a StcoBox),
Co64(&'a Co64Box),
}
#[derive(Debug)]
pub enum StblCoMut<'a> {
pub enum StblCoRefMut<'a> {
Stco(&'a mut StcoBox),
Co64(&'a mut Co64Box),
}
const NAME: BoxType = BoxType::STBL;
const STCO: BoxType = BoxType::STCO;
const CO64: BoxType = BoxType::CO64;
#[non_exhaustive]
#[derive(Clone, Debug, ParseBoxes)]
#[box_type = "stbl"]
struct DerivedStblChildren {
sample_descriptions: StsdBox,
time_to_sample: SttsBox,
sample_to_chunk: StscBox,
sample_sizes: StszBox,
chunk_offsets_32: Option<StcoBox>,
chunk_offsets_64: Option<Co64Box>,
}
impl StblBox {
#[cfg(test)]
pub(crate) fn with_children<C: Into<Boxes>>(children: C) -> Self {
Self { children: children.into() }
pub fn with_children(children: StblChildren) -> Result<Self, ParseError> {
Ok(Self { children: Boxes::new(children, [])? })
}
}
//
// StblCo impls
//
impl ParseBoxes for StblChildren {
type Ref<'a> = StblChildrenRef<'a>;
type RefMut<'a> = StblChildrenRefMut<'a>;
type IntoIter = vec::IntoIter<AnyMp4Box>;
fn parse<'a>(boxes: &'a mut [AnyMp4Box]) -> Result<Self::RefMut<'a>, ParseError>
where
Self: 'a,
{
let DerivedStblChildrenRefMut {
sample_descriptions,
time_to_sample,
sample_to_chunk,
sample_sizes,
chunk_offsets_32,
chunk_offsets_64,
} = DerivedStblChildren::parse(boxes)?;
let chunk_offsets = match (chunk_offsets_32, chunk_offsets_64) {
(Some(chunk_offsets_32), None) => StblCoRefMut::Stco(chunk_offsets_32),
(None, Some(chunk_offsets_64)) => StblCoRefMut::Co64(chunk_offsets_64),
(Some(_), Some(_)) => bail_attach!(
ParseError::InvalidBoxLayout,
"more than one stco and co64 present",
WhileParsingBox(StblBox::NAME),
),
(None, None) => bail_attach!(
ParseError::MissingRequiredBox(StcoBox::NAME),
WhileParsingBox(StblBox::NAME),
),
};
Ok(Self::RefMut { sample_descriptions, time_to_sample, sample_to_chunk, sample_sizes, chunk_offsets })
}
pub fn co_mut(&mut self) -> Result<StblCoMut<'_>, ParseError> {
let have_stco = self.children.box_types().any(|box_type| box_type == STCO);
let have_co64 = self.children.box_types().any(|box_type| box_type == CO64);
ensure_attach!(
!(have_stco && have_co64),
ParseError::InvalidBoxLayout,
"more than one stco and co64 present",
WhileParsingChild(NAME, STCO),
);
if have_stco {
self.children
.get_one_mut()
.while_parsing_child(NAME, STCO)
.map(StblCoMut::Stco)
} else {
self.children
.get_one_mut()
.while_parsing_child(NAME, CO64)
.map(StblCoMut::Co64)
fn parsed<'a>(boxes: &'a [AnyMp4Box]) -> Self::Ref<'a>
where
Self: 'a,
{
let DerivedStblChildrenRef {
sample_descriptions,
time_to_sample,
sample_to_chunk,
sample_sizes,
chunk_offsets_32,
chunk_offsets_64,
} = DerivedStblChildren::parsed(boxes);
let chunk_offsets_32 = chunk_offsets_32.map(StblCoRef::Stco);
let chunk_offsets_64 = chunk_offsets_64.map(StblCoRef::Co64);
let chunk_offsets = chunk_offsets_32.or(chunk_offsets_64).unwrap();
Self::Ref { sample_descriptions, time_to_sample, sample_to_chunk, sample_sizes, chunk_offsets }
}
fn try_into_iter(self) -> Result<Self::IntoIter, ParseError> {
let Self { sample_descriptions, time_to_sample, sample_to_chunk, sample_sizes, chunk_offsets } = self;
let (chunk_offsets_32, chunk_offsets_64) = match chunk_offsets {
StblCo::Stco(chunk_offsets_32) => (Some(chunk_offsets_32), None),
StblCo::Co64(chunk_offsets_64) => (None, Some(chunk_offsets_64)),
};
DerivedStblChildren {
sample_descriptions,
time_to_sample,
sample_to_chunk,
sample_sizes,
chunk_offsets_32,
chunk_offsets_64,
}
.try_into_iter()
}
}
//
// StblCo impls
//
impl Default for StblCo {
fn default() -> Self {
Self::Stco(Default::default())
}
}
//
// StblCoRef impls
//
impl StblCoRef<'_> {
pub fn entry_count(&self) -> u32 {
match self {
Self::Stco(stco) => stco.entry_count(),
Self::Co64(co64) => co64.entry_count(),
}
}
}
@ -54,11 +183,46 @@ impl StblBox {
// StblCoMut impls
//
impl StblCoMut<'_> {
impl StblCoRefMut<'_> {
pub fn entry_count(&self) -> u32 {
match self {
StblCoMut::Stco(stco) => stco.entry_count(),
StblCoMut::Co64(co64) => co64.entry_count(),
Self::Stco(stco) => stco.entry_count(),
Self::Co64(co64) => co64.entry_count(),
}
}
}
#[cfg(test)]
mod test {
use super::*;
impl StblBox {
pub(crate) fn dummy() -> Self {
Self::new(
Default::default(),
Default::default(),
Default::default(),
Default::default(),
Default::default(),
)
.unwrap()
}
#[cfg(test)]
pub(crate) fn new(
sample_descriptions: StsdBox,
time_to_sample: SttsBox,
sample_to_chunk: StscBox,
sample_sizes: StszBox,
chunk_offsets: StblCo,
) -> Result<Self, ParseError> {
Self::with_children(StblChildren {
sample_descriptions,
time_to_sample,
sample_to_chunk,
sample_sizes,
chunk_offsets,
})
}
}
}

View File

@ -1,6 +1,6 @@
#![allow(missing_docs)]
use super::{ArrayEntryMut, BoundedArray, ConstFullBoxHeader, ParseBox, ParsedBox};
use super::{ArrayEntry, ArrayEntryMut, BoundedArray, ConstFullBoxHeader, ParseBox, ParsedBox};
#[derive(Clone, Debug, Default, ParseBox, ParsedBox)]
#[box_type = "stco"]
@ -10,6 +10,10 @@ pub struct StcoBox {
}
impl StcoBox {
pub fn entries(&self) -> impl Iterator<Item = ArrayEntry<'_, u32>> + ExactSizeIterator + '_ {
self.entries.entries()
}
pub fn entries_mut(&mut self) -> impl Iterator<Item = ArrayEntryMut<'_, u32>> + ExactSizeIterator + '_ {
self.entries.entries_mut()
}

View File

@ -0,0 +1,54 @@
#![allow(missing_docs)]
use std::ops::{Deref, DerefMut};
use std::str;
use bytes::{BufMut, BytesMut};
use crate::error::Result;
use super::{Mp4Value, ParseError};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Mp4String {
data: BytesMut,
}
impl Deref for Mp4String {
type Target = str;
fn deref(&self) -> &Self::Target {
str::from_utf8(&self.data).unwrap_or_else(|_| unreachable!())
}
}
impl DerefMut for Mp4String {
fn deref_mut(&mut self) -> &mut Self::Target {
str::from_utf8_mut(&mut self.data).unwrap_or_else(|_| unreachable!())
}
}
impl Mp4Value for Mp4String {
fn parse(buf: &mut BytesMut) -> Result<Self, ParseError> {
let mut data = buf.split();
ensure_attach!(
data.last() == Some(&0),
ParseError::InvalidInput,
"string not null-terminated"
);
let _ = data.split_off(data.len() - 1);
if let Err(err) = str::from_utf8(&data) {
bail_attach!(ParseError::InvalidInput, err);
}
Ok(Self { data })
}
fn encoded_len(&self) -> u64 {
self.data.len() as u64 + 1
}
fn put_buf<B: BufMut>(&self, mut buf: B) {
buf.put(&self.data[..]);
buf.put_u8(0);
}
}

59
mp4san/src/parse/stsc.rs Normal file
View File

@ -0,0 +1,59 @@
#![allow(missing_docs)]
use super::{ArrayEntry, ArrayEntryMut, BoundedArray, ConstFullBoxHeader, Mp4Prim, ParseBox, ParsedBox};
#[derive(Clone, Debug, Default, ParseBox, ParsedBox)]
#[box_type = "stsc"]
pub struct StscBox {
_parsed_header: ConstFullBoxHeader,
entries: BoundedArray<u32, StscEntry>,
}
#[derive(Clone, Copy, Debug, Mp4Prim)]
pub struct StscEntry {
pub first_chunk: u32,
pub samples_per_chunk: u32,
pub samples_description_index: u32,
}
#[derive(Mp4Prim)]
pub enum Test {
A([u8; 4]),
B(u32),
}
impl StscBox {
pub fn entries(&self) -> impl ExactSizeIterator<Item = ArrayEntry<'_, StscEntry>> + '_ {
self.entries.entries()
}
pub fn entries_mut(&mut self) -> impl ExactSizeIterator<Item = ArrayEntryMut<'_, StscEntry>> + '_ {
self.entries.entries_mut()
}
pub fn entry_count(&self) -> u32 {
self.entries.entry_count()
}
}
impl FromIterator<StscEntry> for StscBox {
fn from_iter<I: IntoIterator<Item = StscEntry>>(entries: I) -> Self {
Self { _parsed_header: Default::default(), entries: entries.into_iter().collect() }
}
}
#[cfg(test)]
mod test {
use bytes::BytesMut;
use crate::parse::{ParseBox, ParsedBox};
use super::StscBox;
#[test]
fn roundtrip() {
let mut buf = BytesMut::new();
StscBox::default().put_buf(&mut buf);
StscBox::parse(&mut buf).unwrap();
}
}

14
mp4san/src/parse/stsd.rs Normal file
View File

@ -0,0 +1,14 @@
#![allow(missing_docs)]
use derive_more::{Deref, DerefMut};
use super::{BoundedBoxes, ConstFullBoxHeader, ParseBox, ParsedBox};
#[derive(Clone, Debug, Default, Deref, DerefMut, ParseBox, ParsedBox)]
#[box_type = "stsd"]
pub struct StsdBox {
pub _parsed_header: ConstFullBoxHeader,
#[deref]
#[deref_mut]
pub children: BoundedBoxes<u32>,
}

50
mp4san/src/parse/stsz.rs Normal file
View File

@ -0,0 +1,50 @@
#![allow(missing_docs)]
use std::num::NonZeroU32;
use super::{BoundedArray, ConstFullBoxHeader, ConstU32, Mp4Value, ParseBox, ParseError, ParsedBox};
use crate::error::Result;
#[derive(Clone, Debug, Default, ParseBox, ParsedBox)]
#[box_type = "stsz"]
pub struct StszBox {
_parsed_header: ConstFullBoxHeader,
entries: StszEntries,
}
#[derive(Clone, Debug, Mp4Value)]
enum StszEntries {
VariableSize {
_not_fixed_size: ConstU32,
entries: BoundedArray<u32, u32>,
},
FixedSize {
size: NonZeroU32,
number_of_samples: u32,
},
}
impl Default for StszEntries {
fn default() -> Self {
Self::VariableSize { _not_fixed_size: Default::default(), entries: Default::default() }
}
}
impl StszBox {
pub fn sample_sizes(&self) -> impl ExactSizeIterator<Item = Result<u32, ParseError>> + '_ {
let (mut variable_iter, fixed_size, count) = match &self.entries {
StszEntries::FixedSize { size, number_of_samples } => (None, u32::from(*size), *number_of_samples),
StszEntries::VariableSize { _not_fixed_size, entries } => {
let variable_iter = entries.entries().map(|entry| entry.get());
(Some(variable_iter), 0, entries.entry_count())
}
};
// Handrolled "Either" here:
(0..count).map(move |_| {
variable_iter
.as_mut()
.map_or(Ok(fixed_size), |iter| iter.next().expect("matches count"))
})
}
}

48
mp4san/src/parse/stts.rs Normal file
View File

@ -0,0 +1,48 @@
#![allow(missing_docs)]
use super::{ArrayEntryMut, BoundedArray, ConstFullBoxHeader, Mp4Prim, ParseBox, ParsedBox};
#[derive(Clone, Debug, Default, ParseBox, ParsedBox)]
#[box_type = "stts"]
pub struct SttsBox {
_parsed_header: ConstFullBoxHeader,
entries: BoundedArray<u32, SttsEntry>,
}
#[derive(Clone, Copy, Debug, Mp4Prim)]
pub struct SttsEntry {
pub sample_count: u32,
pub sample_delta: u32,
}
impl SttsBox {
pub fn entries_mut(&mut self) -> impl ExactSizeIterator<Item = ArrayEntryMut<'_, SttsEntry>> + '_ {
self.entries.entries_mut()
}
pub fn entry_count(&self) -> u32 {
self.entries.entry_count()
}
}
impl FromIterator<SttsEntry> for SttsBox {
fn from_iter<I: IntoIterator<Item = SttsEntry>>(entries: I) -> Self {
Self { _parsed_header: Default::default(), entries: entries.into_iter().collect() }
}
}
#[cfg(test)]
mod test {
use bytes::BytesMut;
use crate::parse::{ParseBox, ParsedBox};
use super::SttsBox;
#[test]
fn roundtrip() {
let mut buf = BytesMut::new();
SttsBox::default().put_buf(&mut buf);
SttsBox::parse(&mut buf).unwrap();
}
}

76
mp4san/src/parse/tkhd.rs Normal file
View File

@ -0,0 +1,76 @@
#![allow(missing_docs)]
use fixed::types::I8F8;
use super::{BoxFlags, ConstU16, ConstU32, ConstU8, Mp4Transform, ParseBox, ParsedBox};
#[derive(Clone, Debug, ParseBox, ParsedBox)]
#[box_type = "tkhd"]
pub enum TkhdBox {
V1 {
_version: ConstU8<1>,
flags: BoxFlags,
creation_time: u64,
modification_time: u64,
track_id: u32,
_reserved: ConstU32,
duration: u64,
_reserved_2: [ConstU32; 2],
layer: i16,
alternate_group: i16,
volume: I8F8,
_reserved_3: ConstU16,
matrix: Mp4Transform,
width: u32,
height: u32,
},
V0 {
_version: ConstU8,
flags: BoxFlags,
creation_time: u32,
modification_time: u32,
track_id: u32,
_reserved: ConstU32,
duration: u32,
_reserved_2: [ConstU32; 2],
layer: i16,
alternate_group: i16,
volume: I8F8,
_reserved_3: ConstU16,
matrix: Mp4Transform,
width: u32,
height: u32,
},
}
impl TkhdBox {
pub const DURATION_UNDETERMINED_V0: u32 = u32::MAX;
pub const DURATION_UNDETERMINED_V1: u64 = u64::MAX;
}
#[cfg(test)]
mod test {
use super::*;
impl TkhdBox {
pub(crate) fn dummy() -> Self {
Self::V1 {
_version: Default::default(),
flags: Default::default(),
creation_time: 0,
modification_time: 0,
track_id: 0,
_reserved: Default::default(),
duration: Self::DURATION_UNDETERMINED_V1,
_reserved_2: Default::default(),
layer: 0,
alternate_group: 0,
volume: 1.into(),
_reserved_3: Default::default(),
matrix: Default::default(),
width: 0,
height: 0,
}
}
}
}

View File

@ -1,30 +1,56 @@
#![allow(missing_docs)]
use derive_more::{Deref, DerefMut};
use super::mp4box::Boxes;
use super::{MdiaBox, ParseBox, ParseBoxes, ParseError, ParsedBox, StblCoRef, StblCoRefMut, TkhdBox};
use crate::error::Result;
use super::error::ParseResultExt;
use super::mp4box::Boxes;
use super::{BoxType, MdiaBox, ParseBox, ParseError, ParsedBox, StblCoMut};
#[derive(Clone, Debug, ParseBox, ParsedBox)]
#[derive(Clone, Debug, Deref, DerefMut, ParseBox, ParsedBox)]
#[box_type = "trak"]
pub struct TrakBox {
children: Boxes,
pub children: Boxes<TrakChildren>,
}
const NAME: BoxType = BoxType::TRAK;
#[non_exhaustive]
#[derive(Clone, Debug, ParseBoxes)]
#[box_type = "trak"]
pub struct TrakChildren {
pub header: TkhdBox,
pub media: MdiaBox,
}
impl TrakBox {
#[cfg(test)]
pub(crate) fn with_children<C: Into<Boxes>>(children: C) -> Self {
Self { children: children.into() }
pub fn with_children(children: TrakChildren) -> Result<Self, ParseError> {
Ok(Self { children: Boxes::new(children, [])? })
}
pub fn co_mut(&mut self) -> Result<StblCoMut<'_>, ParseError> {
self.mdia_mut()?.minf_mut()?.stbl_mut()?.co_mut()
pub fn co(&self) -> StblCoRef<'_> {
let media = self.parsed().media;
let info = media.parsed().info;
let samples = info.parsed().samples;
samples.parsed().chunk_offsets
}
pub fn mdia_mut(&mut self) -> Result<&mut MdiaBox, ParseError> {
self.children.get_one_mut().while_parsing_child(NAME, BoxType::MDIA)
pub fn co_mut(&mut self) -> StblCoRefMut<'_> {
let media = self.parsed_mut().media;
let info = media.parsed_mut().info;
let samples = info.parsed_mut().samples;
samples.parsed_mut().chunk_offsets
}
}
#[cfg(test)]
mod test {
use super::*;
impl TrakBox {
pub(crate) fn dummy() -> Self {
Self::new(TkhdBox::dummy(), MdiaBox::dummy()).unwrap()
}
pub(crate) fn new(header: TkhdBox, media: MdiaBox) -> Result<Self, ParseError> {
Self::with_children(TrakChildren { header, media })
}
}
}

View File

@ -10,6 +10,16 @@ pub trait Mp4Value: Sized {
fn parse(buf: &mut BytesMut) -> Result<Self, ParseError>;
fn encoded_len(&self) -> u64;
fn put_buf<B: BufMut>(&self, buf: B);
fn to_bytes(&self) -> BytesMut {
let mut buf = BytesMut::with_capacity(self.encoded_len() as usize);
self.put_buf(&mut buf);
buf
}
fn to_vec(&self) -> Vec<u8> {
self.to_bytes().to_vec()
}
}
pub trait Mp4ValueReaderExt {
@ -27,7 +37,7 @@ impl<T: Mp4Prim> Mp4Value for T {
Self::parse(buf)
}
fn encoded_len(&self) -> u64 {
Self::encoded_len()
Self::ENCODED_LEN
}
fn put_buf<B: BufMut>(&self, buf: B) {
self.put_buf(buf)

View File

@ -7,7 +7,7 @@ use std::iter;
use bytes::{BufMut, BytesMut};
use crate::parse::box_type::{DINF, DREF, HDLR, MDAT, MDHD, MECO, META, METT, MVHD, STSC, STSD, STSZ, STTS, TKHD, URL};
use crate::parse::{fourcc, AnyMp4Box, BoxHeader, BoxType, BoxUuid, FourCC, FullBoxHeader, Mp4Box, Mp4Value};
use crate::parse::{fourcc, AnyMp4Box, BoxFlags, BoxHeader, BoxType, BoxUuid, FourCC, FullBoxHeader, Mp4Box, Mp4Value};
use crate::{InputSpan, SanitizedMetadata};
pub const TEST_UUID: BoxType = BoxType::Uuid(BoxUuid { value: *b"thisisatestuuid!" });
@ -140,7 +140,7 @@ pub fn write_test_dinf_data<B: BufMut>(mut out: B) {
FullBoxHeader::default().put_buf(&mut out);
out.put_u32(1); // entry count
BoxHeader::with_u32_data_size(URL, 4).put_buf(&mut out); // url header
FullBoxHeader { version: 0, flags: 1 }.put_buf(&mut out);
FullBoxHeader { version: 0, flags: BoxFlags(1) }.put_buf(&mut out);
}
pub fn write_test_mdat(out: &mut Vec<u8>, data: &[u8]) -> InputSpan {

View File

@ -1,6 +1,10 @@
use bytes::BytesMut;
use derive_builder::Builder;
use crate::parse::{fourcc, Co64Box, MdiaBox, MinfBox, MoovBox, Mp4Box, StblBox, StcoBox, TrakBox};
use crate::parse::{
fourcc, AnyMp4Box, BoxData, Boxes, Co64Box, MdiaBox, MinfBox, MoovBox, Mp4Box, Mp4Value, ParseBox, ParsedBox,
StblBox, StcoBox, TrakBox,
};
use super::{test_dinf, test_hdlr, test_mdhd, test_mvhd, test_stsc, test_stsd, test_stsz, test_stts, test_tkhd};
@ -47,23 +51,37 @@ impl TestMoovBuilder {
let mut minf = vec![test_dinf()];
if spec.stbl {
minf.push(Mp4Box::with_data(StblBox::with_children(stbl).into()).unwrap().into());
let stbl: Mp4Box<StblBox> = container(stbl);
minf.push(stbl.into());
}
let mut mdia = vec![test_mdhd(), test_hdlr(fourcc::META)];
if spec.minf {
mdia.push(Mp4Box::with_data(MinfBox::with_children(minf).into()).unwrap().into());
let minf: Mp4Box<MinfBox> = container(minf);
mdia.push(minf.into());
}
let mut trak = vec![test_tkhd(1)];
if spec.mdia {
trak.push(Mp4Box::with_data(MdiaBox::with_children(mdia).into()).unwrap().into());
let mdia: Mp4Box<MdiaBox> = container(mdia);
trak.push(mdia.into());
}
let mut moov = vec![test_mvhd()];
if spec.trak {
moov.push(Mp4Box::with_data(TrakBox::with_children(trak).into()).unwrap().into());
let trak: Mp4Box<TrakBox> = container(trak);
moov.push(trak.into());
}
Mp4Box::with_data(MoovBox::with_children(moov).into()).unwrap()
container(moov)
}
}
fn container<T: ParseBox + ParsedBox, I: IntoIterator>(boxes: I) -> Mp4Box<T>
where
AnyMp4Box: From<I::Item>,
{
let mut data = BytesMut::new();
let boxes = boxes.into_iter().map(AnyMp4Box::from).collect::<Vec<_>>();
Boxes::<()>::try_from(boxes).unwrap().put_buf(&mut data);
Mp4Box::with_data(BoxData::Bytes(data.into())).unwrap()
}

@ -1 +1 @@
Subproject commit b2121e38ec04025fdb62306b4863566fa993c8cb
Subproject commit b9f4d3f3273819ce50170357098a56113eb088b2

View File

@ -10,6 +10,6 @@ default = []
libwebp = ["dep:libwebp-sys"]
[dependencies]
libwebp-sys = { workspace = true, optional = true }
log = { workspace = true }
thiserror = { workspace = true }
libwebp-sys = { version = "0.9.4", optional = true }
log = "0.4.17"
thiserror = "1.0.40"

View File

@ -19,21 +19,21 @@ default = ["error-detail"]
error-detail = []
[dependencies]
assert_matches = { workspace = true }
bitflags = { workspace = true }
bitstream-io = { workspace = true }
bytes = { workspace = true }
derive_builder = { workspace = true }
derive_more = { workspace = true, features = ["display"] }
log = { workspace = true }
mediasan-common = { path = "../common", version = "=0.5.3" }
num-integer = { workspace = true }
num-traits = { workspace = true }
thiserror = { workspace = true }
assert_matches = "1.5.0"
bitflags = "2.4.0"
bitstream-io = "1.7.0"
bytes = "1.3.0"
derive_builder = "0.20.0"
derive_more = "0.99.17"
log = "0.4.17"
mediasan-common = { path = "../common", version = "=0.5.2" }
num-integer = { version = "0.1.45", default-features = false }
num-traits = { version = "0.2.16", default-features = false }
thiserror = "1.0.38"
[dev-dependencies]
assert_matches = { workspace = true }
criterion = { workspace = true, features = ["async_futures"] }
assert_matches = "1.5.0"
criterion = { version = "0.5.1", features = ["async_futures"] }
mediasan-common-test = { path = "../common-test" }
webpsan-test = { path = "../webpsan-test" }

View File

@ -72,7 +72,7 @@ trait ReadSkip: Read + Skip {}
type DynChunkReader<'a> = ChunkReader<dyn ReadSkip + 'a>;
#[derive(Clone, Copy, Debug, Display)]
#[display("frame dimensions `{_0}`x`{_1}` do not match canvas dimensions `{_2}`x`{_3}`")]
#[display(fmt = "frame dimensions `{_0}`x`{_1}` do not match canvas dimensions `{_2}`x`{_3}`")]
struct FrameDimensionsMismatch(NonZeroU16, NonZeroU16, NonZeroU32, NonZeroU32);
//

View File

@ -26,7 +26,7 @@ pub struct CanonicalHuffmanTree<E: Endianness, S: Clone> {
}
#[derive(Display)]
#[display("invalid lz77 prefix code `{_0}`")]
#[display(fmt = "invalid lz77 prefix code `{_0}`")]
struct InvalidLz77PrefixCode(u16);
pub const LZ77_MAX_LEN: u16 = (LZ77_MAX_SYMBOL - 2) >> 1;

View File

@ -58,19 +58,19 @@ pub(crate) trait ParseResultExt: ResultExt + Sized {
}
#[derive(Clone, Copy, Debug, Display)]
#[display("multiple `{}` chunks", _0)]
#[display(fmt = "multiple `{}` chunks", _0)]
pub(crate) struct MultipleChunks(pub(crate) FourCC);
#[derive(Clone, Copy, Debug, Display)]
#[display("expected `{}` chunk", _0)]
#[display(fmt = "expected `{}` chunk", _0)]
pub(crate) struct ExpectedChunk(pub(crate) FourCC);
#[derive(Clone, Copy, Debug, Display)]
#[display("while parsing `{}` chunk", _0)]
#[display(fmt = "while parsing `{}` chunk", _0)]
pub(crate) struct WhileParsingChunk(pub(crate) FourCC);
#[derive(Clone, Copy, Debug, Display)]
#[display("while parsing `{}` chunk field `{}`", _0, _1)]
#[display(fmt = "while parsing `{}` chunk field `{}`", _0, _1)]
pub(crate) struct WhileParsingField<T>(pub(crate) FourCC, pub(crate) T);
impl ReportableError for ParseError {

View File

@ -26,26 +26,26 @@ pub struct LosslessImage {
#[derive(Clone, Display, PartialEq, Eq)]
enum Transform {
#[display("predictor transform: block size {block_size}")]
#[display(fmt = "predictor transform: block size {block_size}")]
Predictor { block_size: u16, _image: EntropyCodedImage },
#[display("color transform: block size {block_size}")]
#[display(fmt = "color transform: block size {block_size}")]
Color { block_size: u16, _image: EntropyCodedImage },
#[display("subtract green transform")]
#[display(fmt = "subtract green transform")]
SubtractGreen,
#[display("color indexing transform: {} colors", "image.width")]
#[display(fmt = "color indexing transform: {} colors", "image.width")]
ColorIndexing { image: EntropyCodedImage },
}
#[repr(u8)]
#[derive(Clone, Copy, Debug, Display, PartialEq, Eq, PartialOrd, Ord)]
enum TransformType {
#[display("predictor")]
#[display(fmt = "predictor")]
Predictor = 0b00,
#[display("color")]
#[display(fmt = "color")]
Color = 0b01,
#[display("subtract green")]
#[display(fmt = "subtract green")]
SubtractGreen = 0b10,
#[display("color indexing")]
#[display(fmt = "color indexing")]
ColorIndexing = 0b11,
}
@ -59,14 +59,14 @@ struct EntropyCodedImage {
struct SpatiallyCodedImage;
#[derive(Clone, Copy, Debug, Display, PartialEq, Eq)]
#[display("distance {dist} length {len}")]
#[display(fmt = "distance {dist} length {len}")]
struct BackReference {
dist: NonZeroU32,
len: NonZeroU32,
}
#[derive(Clone, Copy, Debug, Default, Display, PartialEq, Eq)]
#[display("({alpha}, {red}, {green}, {blue})")]
#[display(fmt = "({alpha}, {red}, {green}, {blue})")]
struct Color {
alpha: u8,
red: u8,
@ -81,9 +81,9 @@ struct ColorCache {
#[derive(Clone, Display, PartialEq, Eq)]
enum MetaPrefixCodes {
#[display("single meta prefix code")]
#[display(fmt = "single meta prefix code")]
Single,
#[display("multiple meta prefix codes: max code group {max_code_group}, block size {block_size}")]
#[display(fmt = "multiple meta prefix codes: max code group {max_code_group}, block size {block_size}")]
Multiple {
block_size: u16,
max_code_group: u16,
@ -124,39 +124,39 @@ struct DistancePrefixCode {
}
#[derive(Clone, Copy, Debug, Display)]
#[display("out-of-bounds color cache index `{_0}` >= `{_1}`")]
#[display(fmt = "out-of-bounds color cache index `{_0}` >= `{_1}`")]
struct ColorCacheIndexOutOfBounds(u16, u16);
#[derive(Clone, Copy, Debug, Display)]
#[display("invalid back-reference distance `{_0}` at pixel `{_1}`")]
#[display(fmt = "invalid back-reference distance `{_0}` at pixel `{_1}`")]
struct InvalidBackRefDistance(NonZeroU32, u32);
#[derive(Clone, Copy, Debug, Display)]
#[display("invalid back-reference length `{_0}` at pixel `{_1}` with image length `{_2}`")]
#[display(fmt = "invalid back-reference length `{_0}` at pixel `{_1}` with image length `{_2}`")]
struct InvalidBackRefLength(NonZeroU32, u32, u32);
#[derive(Clone, Copy, Debug, Display)]
#[display("invalid code length repetition `{_0}` at `{_1}` with max symbols `{_2}`")]
#[display(fmt = "invalid code length repetition `{_0}` at `{_1}` with max symbols `{_2}`")]
struct InvalidCodeLengthRepetition(u8, usize, u16);
#[derive(Clone, Copy, Debug, Display)]
#[display("invalid color cache size `{_0}`")]
#[display(fmt = "invalid color cache size `{_0}`")]
struct InvalidColorCacheSize(u8);
#[derive(Clone, Copy, Debug, Display)]
#[display("invalid duplicate {_0} transform")]
#[display(fmt = "invalid duplicate {_0} transform")]
struct InvalidDuplicateTransform(TransformType);
#[derive(Clone, Copy, Debug, Display)]
#[display("invalid predictor `{_0}`")]
#[display(fmt = "invalid predictor `{_0}`")]
struct InvalidPredictor(u8);
#[derive(Clone, Copy, Debug, Display)]
#[display("invalid symbol count `{_0}` >= `{_1}`")]
#[display(fmt = "invalid symbol count `{_0}` >= `{_1}`")]
struct InvalidSymbolCount(u16, u16);
#[derive(Clone, Copy, Debug, Display)]
#[display("while parsing {_0} transform")]
#[display(fmt = "while parsing {_0} transform")]
struct WhileParsingTransform(TransformType);
//

View File

@ -31,7 +31,7 @@ pub struct Vp8lChunk {
//
#[derive(Clone, Copy, Debug, Display)]
#[display("invalid VP8L signature `0x{_0:x}` != `0x{}`", Vp8lChunk::SIGNATURE)]
#[display(fmt = "invalid VP8L signature `0x{_0:x}` != `0x{}`", Vp8lChunk::SIGNATURE)]
struct InvalidSignature(u8);
//