use std::{
cmp::{max, min},
fmt::{Debug, Display},
fs::File,
io::{self, Read, Seek},
path::Path,
};
use crate::{
error::{ParsedError, ParsingError, ParsingErrorState},
exif::TiffHeader,
file::MediaMime,
ExifIter, TrackInfo,
};
pub(crate) type SkipBySeekFn<R> = fn(&mut R, u64) -> io::Result<bool>;
pub struct MediaSource<R> {
pub(crate) reader: R,
pub(crate) buf: Vec<u8>,
pub(crate) mime: MediaMime,
pub(crate) skip_by_seek: SkipBySeekFn<R>,
pub(crate) memory: Option<bytes::Bytes>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MediaKind {
Image,
Track,
}
impl<R> Debug for MediaSource<R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MediaSource")
.field("mime", &self.mime)
.finish_non_exhaustive()
}
}
const HEADER_PARSE_BUF_SIZE: usize = 128;
impl<R> MediaSource<R> {
pub fn kind(&self) -> MediaKind {
match self.mime {
MediaMime::Image(_) => MediaKind::Image,
MediaMime::Track(_) => MediaKind::Track,
}
}
}
impl<R: Read> MediaSource<R> {
fn build(mut reader: R, skip_by_seek: SkipBySeekFn<R>) -> crate::Result<Self> {
let mut buf = Vec::with_capacity(HEADER_PARSE_BUF_SIZE);
reader
.by_ref()
.take(HEADER_PARSE_BUF_SIZE as u64)
.read_to_end(&mut buf)?;
let mime: MediaMime = buf.as_slice().try_into()?;
Ok(Self {
reader,
buf,
mime,
skip_by_seek,
memory: None,
})
}
pub fn unseekable(reader: R) -> crate::Result<Self> {
Self::build(reader, |_, _| Ok(false))
}
}
impl<R: Read + Seek> MediaSource<R> {
pub fn seekable(reader: R) -> crate::Result<Self> {
Self::build(reader, |r, n| {
let signed: i64 = n
.try_into()
.map_err(|_| io::Error::from(io::ErrorKind::InvalidInput))?;
r.seek_relative(signed)?;
Ok(true)
})
}
}
impl MediaSource<File> {
pub fn open<P: AsRef<Path>>(path: P) -> crate::Result<Self> {
Self::seekable(File::open(path)?)
}
}
impl MediaSource<()> {
pub fn from_bytes(bytes: impl Into<bytes::Bytes>) -> crate::Result<Self> {
let bytes = bytes.into();
let head_end = bytes.len().min(HEADER_PARSE_BUF_SIZE);
let mime: MediaMime = bytes[..head_end].try_into()?;
Ok(Self {
reader: (),
buf: Vec::new(),
mime,
skip_by_seek: |_, _| Ok(false),
memory: Some(bytes),
})
}
}
pub(crate) const INIT_BUF_SIZE: usize = 8 * 1024;
pub(crate) const MIN_GROW_SIZE: usize = 16 * 1024;
pub(crate) const MAX_PARSE_BUF_SIZE: usize = 1024 * 1024 * 1024;
const MAX_REUSE_BUF_SIZE: usize = 4 * 1024 * 1024;
pub(crate) trait Buf {
fn buffer(&self) -> &[u8];
fn clear(&mut self);
fn set_position(&mut self, pos: usize);
#[allow(unused)]
fn position(&self) -> usize;
}
#[derive(Debug, Default)]
pub(crate) struct BufferedParserState {
cached: Option<bytes::Bytes>,
buf: Option<Vec<u8>>,
memory: Option<bytes::Bytes>,
position: usize,
}
impl BufferedParserState {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn reset(&mut self) {
self.buf = None;
self.memory = None;
self.position = 0;
}
pub(crate) fn set_memory(&mut self, bytes: bytes::Bytes) {
debug_assert!(
self.buf.is_none() && self.memory.is_none(),
"set_memory called on non-clean state"
);
self.memory = Some(bytes);
self.position = 0;
}
pub(crate) fn is_memory_mode(&self) -> bool {
self.memory.is_some()
}
pub(crate) fn acquire_buf(&mut self) {
if self.memory.is_some() {
return;
}
debug_assert!(self.buf.is_none());
let buf = match self.cached.take() {
Some(b) => match b.try_into_mut() {
Ok(bm) => {
let mut v = Vec::<u8>::from(bm);
v.clear();
if v.capacity() > MAX_REUSE_BUF_SIZE {
v.shrink_to(MAX_REUSE_BUF_SIZE);
}
v
}
Err(_still_shared) => Vec::with_capacity(INIT_BUF_SIZE),
},
None => Vec::with_capacity(INIT_BUF_SIZE),
};
self.buf = Some(buf);
}
pub(crate) fn buf(&self) -> &Vec<u8> {
self.buf.as_ref().expect("no buf here")
}
pub(crate) fn buf_mut(&mut self) -> &mut Vec<u8> {
self.buf.as_mut().expect("no buf here")
}
#[cfg(test)]
pub(crate) fn cached_ptr_for_test(&self) -> Option<*const u8> {
self.cached.as_ref().map(|b| b.as_ptr())
}
#[cfg(test)]
pub(crate) fn buf_is_none_for_test(&self) -> bool {
self.buf.is_none()
}
}
impl Buf for BufferedParserState {
fn buffer(&self) -> &[u8] {
if let Some(m) = &self.memory {
return &m[self.position..];
}
&self.buf()[self.position..]
}
fn clear(&mut self) {
if self.memory.is_some() {
return;
}
self.buf_mut().clear();
}
fn set_position(&mut self, pos: usize) {
self.position = pos;
}
fn position(&self) -> usize {
self.position
}
}
impl ShareBuf for BufferedParserState {
fn share_buf(&mut self) -> (bytes::Bytes, usize) {
if let Some(m) = self.memory.take() {
let position = self.position;
return (m, position);
}
let vec = self.buf.take().expect("no buf to share");
let bytes = bytes::Bytes::from(vec);
let position = self.position;
self.cached = Some(bytes.clone());
(bytes, position)
}
}
pub(crate) enum SkipPlan {
AdvanceOnly,
ClearAndSkip { extra: usize },
}
pub(crate) fn clear_and_skip_decide(buffer_len: usize, n: usize) -> SkipPlan {
if n <= buffer_len {
SkipPlan::AdvanceOnly
} else {
SkipPlan::ClearAndSkip {
extra: n - buffer_len,
}
}
}
pub(crate) fn check_fill_size(existing_len: usize, requested: usize) -> io::Result<()> {
if requested.saturating_add(existing_len) > MAX_PARSE_BUF_SIZE {
tracing::error!(?requested, "the requested buffer size is too big");
return Err(io::ErrorKind::Unsupported.into());
}
Ok(())
}
pub(crate) enum LoopAction<O> {
Done(O),
NeedFill(usize),
Skip(usize),
Failed(String),
}
pub(crate) type ParseFn<'a, O> =
dyn FnMut(&[u8], usize, Option<ParsingState>) -> Result<O, ParsingErrorState> + 'a;
pub(crate) fn parse_loop_step<O>(
buffer: &[u8],
offset: usize,
parsing_state: &mut Option<ParsingState>,
parse: &mut ParseFn<'_, O>,
) -> LoopAction<O> {
match parse(buffer, offset, parsing_state.take()) {
Ok(o) => LoopAction::Done(o),
Err(es) => {
*parsing_state = es.state;
match es.err {
ParsingError::Need(n) => LoopAction::NeedFill(n),
ParsingError::ClearAndSkip(n) => LoopAction::Skip(n),
ParsingError::Failed(s) => LoopAction::Failed(s),
}
}
}
}
#[derive(Debug, Clone)]
pub(crate) enum ParsingState {
TiffHeader(TiffHeader),
HeifExifSize(usize),
Cr3ExifSize(usize),
}
impl Display for ParsingState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ParsingState::TiffHeader(h) => Display::fmt(&format!("ParsingState: {h:?})"), f),
ParsingState::HeifExifSize(n) => Display::fmt(&format!("ParsingState: {n}"), f),
ParsingState::Cr3ExifSize(n) => Display::fmt(&format!("ParsingState: {n}"), f),
}
}
}
pub(crate) trait BufParser: Buf + Debug {
fn fill_buf<R: Read>(&mut self, reader: &mut R, size: usize) -> io::Result<usize>;
fn load_and_parse<R: Read, P, O>(
&mut self,
reader: &mut R,
skip_by_seek: SkipBySeekFn<R>,
mut parse: P,
) -> Result<O, ParsedError>
where
P: FnMut(&[u8], Option<ParsingState>) -> Result<O, ParsingErrorState>,
{
self.load_and_parse_with_offset(
reader,
skip_by_seek,
|data, _, state| parse(data, state),
0,
)
}
#[tracing::instrument(skip_all)]
fn load_and_parse_with_offset<R: Read, P, O>(
&mut self,
reader: &mut R,
skip_by_seek: SkipBySeekFn<R>,
mut parse: P,
offset: usize,
) -> Result<O, ParsedError>
where
P: FnMut(&[u8], usize, Option<ParsingState>) -> Result<O, ParsingErrorState>,
{
if offset >= self.buffer().len() {
self.fill_buf(reader, MIN_GROW_SIZE)?;
}
let mut parsing_state: Option<ParsingState> = None;
loop {
match parse_loop_step(self.buffer(), offset, &mut parsing_state, &mut parse) {
LoopAction::Done(o) => return Ok(o),
LoopAction::NeedFill(needed) => {
let to_read = max(needed, MIN_GROW_SIZE);
let n = self.fill_buf(reader, to_read)?;
if n == 0 {
return Err(ParsedError::NoEnoughBytes);
}
}
LoopAction::Skip(n) => {
self.clear_and_skip(reader, skip_by_seek, n)?;
}
LoopAction::Failed(s) => return Err(ParsedError::Failed(s)),
}
}
}
#[tracing::instrument(skip(reader, skip_by_seek))]
fn clear_and_skip<R: Read>(
&mut self,
reader: &mut R,
skip_by_seek: SkipBySeekFn<R>,
n: usize,
) -> Result<(), ParsedError> {
match clear_and_skip_decide(self.buffer().len(), n) {
SkipPlan::AdvanceOnly => {
self.set_position(self.position() + n);
Ok(())
}
SkipPlan::ClearAndSkip { extra: skip_n } => {
self.clear();
let done = (skip_by_seek)(
reader,
skip_n
.try_into()
.map_err(|_| ParsedError::Failed("skip too many bytes".into()))?,
)?;
if !done {
let mut skipped = 0;
while skipped < skip_n {
let mut to_skip = skip_n - skipped;
to_skip = min(to_skip, MAX_PARSE_BUF_SIZE);
let n = self.fill_buf(reader, to_skip)?;
skipped += n;
if skipped <= skip_n {
self.clear();
} else {
let remain = skipped - skip_n;
self.set_position(self.buffer().len() - remain);
break;
}
}
}
if self.buffer().is_empty() {
self.fill_buf(reader, MIN_GROW_SIZE)?;
}
Ok(())
}
}
}
}
impl BufParser for MediaParser {
#[tracing::instrument(skip(self, reader), fields(buf_len=self.state.buffer().len()))]
fn fill_buf<R: Read>(&mut self, reader: &mut R, size: usize) -> io::Result<usize> {
if self.state.is_memory_mode() {
return Err(std::io::ErrorKind::UnexpectedEof.into());
}
check_fill_size(self.state.buf().len(), size)?;
let n = reader.take(size as u64).read_to_end(self.state.buf_mut())?;
if n == 0 {
tracing::error!(buf_len = self.state.buf().len(), "fill_buf: EOF");
return Err(std::io::ErrorKind::UnexpectedEof.into());
}
tracing::debug!(
?size,
?n,
buf_len = self.state.buf().len(),
"fill_buf: read bytes"
);
Ok(n)
}
}
impl Buf for MediaParser {
fn buffer(&self) -> &[u8] {
self.state.buffer()
}
fn clear(&mut self) {
self.state.clear();
}
fn set_position(&mut self, pos: usize) {
self.state.set_position(pos);
}
fn position(&self) -> usize {
self.state.position()
}
}
pub struct MediaParser {
state: BufferedParserState,
}
impl Debug for MediaParser {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MediaParser")
.field("state", &self.state)
.finish_non_exhaustive()
}
}
impl Default for MediaParser {
fn default() -> Self {
Self {
state: BufferedParserState::new(),
}
}
}
pub(crate) trait ShareBuf {
fn share_buf(&mut self) -> (bytes::Bytes, usize);
}
impl ShareBuf for MediaParser {
fn share_buf(&mut self) -> (bytes::Bytes, usize) {
self.state.share_buf()
}
}
impl MediaParser {
pub fn new() -> Self {
Self::default()
}
pub fn parse_exif<R: Read>(&mut self, mut ms: MediaSource<R>) -> crate::Result<ExifIter> {
self.reset();
self.acquire_buf();
self.buf_mut().append(&mut ms.buf);
let res: crate::Result<ExifIter> = (|| {
self.fill_buf(&mut ms.reader, INIT_BUF_SIZE)?;
if !matches!(ms.mime, crate::file::MediaMime::Image(_)) {
return Err(crate::Error::ExifNotFound);
}
crate::exif::parse_exif_iter(
self,
ms.mime.unwrap_image(),
&mut ms.reader,
ms.skip_by_seek,
)
})();
self.reset();
res
}
pub fn parse_track<R: Read>(&mut self, mut ms: MediaSource<R>) -> crate::Result<TrackInfo> {
self.reset();
self.acquire_buf();
self.buf_mut().append(&mut ms.buf);
let res: crate::Result<TrackInfo> = (|| {
self.fill_buf(&mut ms.reader, INIT_BUF_SIZE)?;
match ms.mime {
crate::file::MediaMime::Image(crate::file::MediaMimeImage::Jpeg) => {
self.parse_jpeg_motion_photo(&mut ms.reader)
}
crate::file::MediaMime::Image(_) => Err(crate::Error::TrackNotFound),
crate::file::MediaMime::Track(mime_track) => {
let skip = ms.skip_by_seek;
Ok(self.load_and_parse(ms.reader.by_ref(), skip, |data, _| {
crate::video::parse_track_info(data, mime_track)
.map_err(|e| ParsingErrorState::new(e, None))
})?)
}
}
})();
self.reset();
res
}
fn parse_jpeg_motion_photo<R: Read>(&mut self, reader: &mut R) -> crate::Result<TrackInfo> {
reader.read_to_end(self.buf_mut())?;
let buf = self.buf_mut();
let Some(offset) = crate::jpeg::find_motion_photo_offset(buf) else {
return Err(crate::Error::TrackNotFound);
};
let trailer_start = (buf.len() as u64)
.checked_sub(offset)
.ok_or(crate::Error::TrackNotFound)? as usize;
let trailer = &buf[trailer_start..];
let trailer_mime = crate::file::MediaMime::try_from(trailer)
.map_err(|_| crate::Error::TrackNotFound)?;
let mime_track = match trailer_mime {
crate::file::MediaMime::Track(t) => t,
crate::file::MediaMime::Image(_) => return Err(crate::Error::TrackNotFound),
};
crate::video::parse_track_info(trailer, mime_track).map_err(|e| match e {
crate::error::ParsingError::Need(_)
| crate::error::ParsingError::ClearAndSkip(_) => crate::Error::UnexpectedEof {
context: "motion-photo trailer",
},
crate::error::ParsingError::Failed(msg) => crate::Error::Malformed {
kind: crate::error::MalformedKind::IsoBmffBox,
message: msg,
},
})
}
pub fn parse_exif_from_bytes(&mut self, mut ms: MediaSource<()>) -> crate::Result<ExifIter> {
self.reset();
let memory = ms
.memory
.take()
.expect("MediaSource<()> must have memory (only constructor is from_bytes)");
self.state.set_memory(memory);
let res: crate::Result<ExifIter> = (|| {
if !matches!(ms.mime, crate::file::MediaMime::Image(_)) {
return Err(crate::Error::ExifNotFound);
}
let mut empty = std::io::empty();
crate::exif::parse_exif_iter(
self,
ms.mime.unwrap_image(),
&mut empty,
|_, _| Ok(false),
)
})();
self.reset();
res
}
pub fn parse_track_from_bytes(&mut self, mut ms: MediaSource<()>) -> crate::Result<TrackInfo> {
self.reset();
let memory = ms
.memory
.take()
.expect("MediaSource<()> must have memory (only constructor is from_bytes)");
self.state.set_memory(memory);
let res: crate::Result<TrackInfo> = (|| {
let mime_track = match ms.mime {
crate::file::MediaMime::Image(_) => return Err(crate::Error::TrackNotFound),
crate::file::MediaMime::Track(t) => t,
};
let mut empty = std::io::empty();
let out = self.load_and_parse(
&mut empty,
|_, _| Ok(false),
|data, _| {
crate::video::parse_track_info(data, mime_track)
.map_err(|e| ParsingErrorState::new(e, None))
},
)?;
Ok(out)
})();
self.reset();
res
}
fn reset(&mut self) {
self.state.reset();
}
fn buf_mut(&mut self) -> &mut Vec<u8> {
self.state.buf_mut()
}
fn acquire_buf(&mut self) {
self.state.acquire_buf();
}
}
#[cfg(feature = "tokio")]
mod tokio_impl {
use super::*;
use crate::error::ParsingErrorState;
use crate::parser_async::{AsyncBufParser, AsyncMediaSource};
use tokio::io::{AsyncRead, AsyncReadExt};
impl AsyncBufParser for MediaParser {
async fn fill_buf<R: AsyncRead + Unpin>(
&mut self,
reader: &mut R,
size: usize,
) -> std::io::Result<usize> {
if self.state.is_memory_mode() {
return Err(std::io::ErrorKind::UnexpectedEof.into());
}
check_fill_size(self.state.buf().len(), size)?;
let n = reader
.take(size as u64)
.read_to_end(self.state.buf_mut())
.await?;
if n == 0 {
return Err(std::io::ErrorKind::UnexpectedEof.into());
}
Ok(n)
}
}
impl MediaParser {
pub async fn parse_exif_async<R: AsyncRead + Unpin + Send>(
&mut self,
mut ms: AsyncMediaSource<R>,
) -> crate::Result<ExifIter> {
self.reset();
self.acquire_buf();
self.buf_mut().append(&mut ms.buf);
let res: crate::Result<ExifIter> = async {
<Self as AsyncBufParser>::fill_buf(self, &mut ms.reader, INIT_BUF_SIZE).await?;
if !matches!(ms.mime, crate::file::MediaMime::Image(_)) {
return Err(crate::Error::ExifNotFound);
}
crate::exif::parse_exif_iter_async(
self,
ms.mime.unwrap_image(),
&mut ms.reader,
ms.skip_by_seek,
)
.await
}
.await;
self.reset();
res
}
pub async fn parse_track_async<R: AsyncRead + Unpin + Send>(
&mut self,
mut ms: AsyncMediaSource<R>,
) -> crate::Result<TrackInfo> {
self.reset();
self.acquire_buf();
self.buf_mut().append(&mut ms.buf);
let res: crate::Result<TrackInfo> = async {
<Self as AsyncBufParser>::fill_buf(self, &mut ms.reader, INIT_BUF_SIZE).await?;
let mime_track = match ms.mime {
crate::file::MediaMime::Image(_) => return Err(crate::Error::TrackNotFound),
crate::file::MediaMime::Track(t) => t,
};
let skip = ms.skip_by_seek;
let out = <Self as AsyncBufParser>::load_and_parse(
self,
&mut ms.reader,
skip,
|data, _| {
crate::video::parse_track_info(data, mime_track)
.map_err(|e| ParsingErrorState::new(e, None))
},
)
.await?;
Ok(out)
}
.await;
self.reset();
res
}
}
}
#[cfg(test)]
mod tests {
use std::sync::{LazyLock, Mutex, MutexGuard};
use super::*;
use test_case::case;
enum TrackExif {
Track,
Exif,
NoData,
Invalid,
}
use TrackExif::*;
static PARSER: LazyLock<Mutex<MediaParser>> = LazyLock::new(|| Mutex::new(MediaParser::new()));
fn parser() -> MutexGuard<'static, MediaParser> {
PARSER.lock().unwrap()
}
#[case("3gp_640x360.3gp", Track)]
#[case("broken.jpg", Exif)]
#[case("compatible-brands-fail.heic", Invalid)]
#[case("compatible-brands-fail.mov", Invalid)]
#[case("compatible-brands.heic", NoData)]
#[case("compatible-brands.mov", NoData)]
#[case("embedded-in-heic.mov", Track)]
#[case("exif.heic", Exif)]
#[case("exif.jpg", Exif)]
#[case("exif-no-tz.jpg", Exif)]
#[case("fujifilm_x_t1_01.raf.meta", Exif)]
#[case("meta.mov", Track)]
#[case("meta.mp4", Track)]
#[case("mka.mka", Track)]
#[case("mkv_640x360.mkv", Track)]
#[case("exif-one-entry.heic", Exif)]
#[case("no-exif.jpg", NoData)]
#[case("tif.tif", Exif)]
#[case("ramdisk.img", Invalid)]
#[case("webm_480.webm", Track)]
fn parse_media(path: &str, te: TrackExif) {
let mut parser = parser();
let ms = MediaSource::open(Path::new("testdata").join(path));
match te {
Track => {
let ms = ms.unwrap();
assert_eq!(ms.kind(), MediaKind::Track);
let _: TrackInfo = parser.parse_track(ms).unwrap();
}
Exif => {
let ms = ms.unwrap();
assert_eq!(ms.kind(), MediaKind::Image);
let mut it: ExifIter = parser.parse_exif(ms).unwrap();
let _ = it.parse_gps();
if path.contains("one-entry") {
assert!(it.next().is_some());
assert!(it.next().is_none());
let exif: crate::Exif = it.clone_rewound().into();
assert!(exif.get(ExifTag::Orientation).is_some());
} else {
let _: crate::Exif = it.clone_rewound().into();
}
}
NoData => {
let ms = ms.unwrap();
match ms.kind() {
MediaKind::Image => {
let res = parser.parse_exif(ms);
res.unwrap_err();
}
MediaKind::Track => {
let res = parser.parse_track(ms);
res.unwrap_err();
}
}
}
Invalid => {
ms.unwrap_err();
}
}
}
use crate::testkit::open_sample;
use crate::{EntryValue, Exif, ExifTag, TrackInfoTag};
use chrono::{DateTime, FixedOffset, NaiveDateTime};
use test_case::test_case;
#[test_case("exif.jpg", ExifTag::DateTimeOriginal, DateTime::parse_from_str("2023-07-09T20:36:33+08:00", "%+").unwrap().into())]
#[test_case("exif.heic", ExifTag::DateTimeOriginal, DateTime::parse_from_str("2022-07-22T21:26:32+08:00", "%+").unwrap().into())]
#[test_case("exif.jpg", ExifTag::DateTimeOriginal,
(NaiveDateTime::parse_from_str("2023-07-09T20:36:33", "%Y-%m-%dT%H:%M:%S").unwrap(),
Some(FixedOffset::east_opt(8*3600).unwrap())).into())]
#[test_case("exif-no-tz.jpg", ExifTag::DateTimeOriginal,
(NaiveDateTime::parse_from_str("2023-07-09T20:36:33", "%Y-%m-%dT%H:%M:%S").unwrap(), None).into())]
fn parse_exif(path: &str, tag: ExifTag, v: EntryValue) {
let mut parser = parser();
let mf = MediaSource::seekable(open_sample(path).unwrap()).unwrap();
assert_eq!(mf.kind(), MediaKind::Image);
let iter: ExifIter = parser.parse_exif(mf).unwrap();
let exif: Exif = iter.into();
assert_eq!(exif.get(tag).unwrap(), &v);
let mf = MediaSource::unseekable(open_sample(path).unwrap()).unwrap();
assert_eq!(mf.kind(), MediaKind::Image);
let iter: ExifIter = parser.parse_exif(mf).unwrap();
let exif: Exif = iter.into();
assert_eq!(exif.get(tag).unwrap(), &v);
}
use crate::video::TrackInfoTag::*;
#[test_case("mkv_640x360.mkv", Width, 640_u32.into())]
#[test_case("mkv_640x360.mkv", Height, 360_u32.into())]
#[test_case("mkv_640x360.mkv", DurationMs, 13346_u64.into())]
#[test_case("mkv_640x360.mkv", CreateDate, DateTime::parse_from_str("2008-08-08T08:08:08Z", "%+").unwrap().into())]
#[test_case("meta.mov", Make, "Apple".into())]
#[test_case("meta.mov", Model, "iPhone X".into())]
#[test_case("meta.mov", GpsIso6709, "+27.1281+100.2508+000.000/".into())]
#[test_case("meta.mov", CreateDate, DateTime::parse_from_str("2019-02-12T15:27:12+08:00", "%+").unwrap().into())]
#[test_case("meta.mp4", Width, 1920_u32.into())]
#[test_case("meta.mp4", Height, 1080_u32.into())]
#[test_case("meta.mp4", DurationMs, 1063_u64.into())]
#[test_case("meta.mp4", GpsIso6709, "+27.2939+112.6932/".into())]
#[test_case("meta.mp4", CreateDate, DateTime::parse_from_str("2024-02-03T07:05:38Z", "%+").unwrap().into())]
#[test_case("udta.auth.mp4", Author, "ReplayKitRecording".into(); "udta author")]
#[test_case("auth.mov", Author, "ReplayKitRecording".into(); "mov author")]
#[test_case("sony-a7-xavc.MP4", Width, 1920_u32.into())]
#[test_case("sony-a7-xavc.MP4", Height, 1080_u32.into())]
#[test_case("sony-a7-xavc.MP4", DurationMs, 1440_u64.into())]
#[test_case("sony-a7-xavc.MP4", CreateDate, DateTime::parse_from_str("2026-04-26T09:25:15+00:00", "%+").unwrap().into())]
fn parse_track_info(path: &str, tag: TrackInfoTag, v: EntryValue) {
let mut parser = parser();
let mf = MediaSource::seekable(open_sample(path).unwrap()).unwrap();
let info: TrackInfo = parser.parse_track(mf).unwrap();
assert_eq!(info.get(tag).unwrap(), &v);
let mf = MediaSource::unseekable(open_sample(path).unwrap()).unwrap();
let info: TrackInfo = parser.parse_track(mf).unwrap();
assert_eq!(info.get(tag).unwrap(), &v);
}
#[test_case("crash_moov-trak")]
#[test_case("crash_skip_large")]
#[test_case("crash_add_large")]
fn parse_track_crash(path: &str) {
let mut parser = parser();
let mf = MediaSource::seekable(open_sample(path).unwrap()).unwrap();
let _: TrackInfo = parser.parse_track(mf).unwrap_or_default();
let mf = MediaSource::unseekable(open_sample(path).unwrap()).unwrap();
let _: TrackInfo = parser.parse_track(mf).unwrap_or_default();
}
#[test]
fn parse_oom_large_box() {
let mut parser = parser();
let mf = MediaSource::seekable(open_sample("oom_large_box.heic").unwrap()).unwrap();
let _: Result<ExifIter, _> = parser.parse_exif(mf);
let mf = MediaSource::unseekable(open_sample("oom_large_box.heic").unwrap()).unwrap();
let _: Result<ExifIter, _> = parser.parse_exif(mf);
let mf = MediaSource::seekable(open_sample("oom_large_box.heic").unwrap()).unwrap();
let _: TrackInfo = parser.parse_track(mf).unwrap_or_default();
let mf = MediaSource::unseekable(open_sample("oom_large_box.heic").unwrap()).unwrap();
let _: TrackInfo = parser.parse_track(mf).unwrap_or_default();
}
#[test]
fn media_kind_classifies_image_and_track() {
let img = MediaSource::open("testdata/exif.jpg").unwrap();
assert_eq!(img.kind(), MediaKind::Image);
let trk = MediaSource::open("testdata/meta.mov").unwrap();
assert_eq!(trk.kind(), MediaKind::Track);
}
#[test]
fn media_source_open() {
let ms = MediaSource::open("testdata/exif.jpg").unwrap();
assert_eq!(ms.kind(), MediaKind::Image);
}
#[test]
fn parse_exif_returns_exif_iter() {
let mut parser = parser();
let ms = MediaSource::open("testdata/exif.jpg").unwrap();
let _: ExifIter = parser.parse_exif(ms).unwrap();
}
#[test]
fn parse_track_returns_track_info() {
let mut parser = parser();
let ms = MediaSource::open("testdata/meta.mov").unwrap();
let _: TrackInfo = parser.parse_track(ms).unwrap();
}
#[test]
fn parse_exif_on_track_returns_exif_not_found_v3() {
let mut parser = parser();
let ms = MediaSource::open("testdata/meta.mov").unwrap();
let res = parser.parse_exif(ms);
assert!(matches!(res, Err(crate::Error::ExifNotFound)));
}
#[test]
fn parse_track_on_image_returns_track_not_found_v3() {
let mut parser = parser();
let ms = MediaSource::open("testdata/exif.jpg").unwrap();
let res = parser.parse_track(ms);
assert!(matches!(res, Err(crate::Error::TrackNotFound)));
}
#[cfg(feature = "tokio")]
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn media_parser_parse_exif_async() {
use crate::parser_async::AsyncMediaSource;
let mut parser = MediaParser::new();
let ms = AsyncMediaSource::open("testdata/exif.jpg").await.unwrap();
let _: ExifIter = parser.parse_exif_async(ms).await.unwrap();
}
#[cfg(feature = "tokio")]
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn media_parser_parse_track_async() {
use crate::parser_async::AsyncMediaSource;
let mut parser = MediaParser::new();
let ms = AsyncMediaSource::open("testdata/meta.mov").await.unwrap();
let _: TrackInfo = parser.parse_track_async(ms).await.unwrap();
}
#[test]
fn parser_recycles_alloc_when_exif_iter_dropped() {
let mut parser = MediaParser::new();
let ms = MediaSource::open("testdata/exif.jpg").unwrap();
let iter = parser.parse_exif(ms).unwrap();
let exif: crate::Exif = iter.into();
drop(exif);
let ptr_after_first = parser.state.cached_ptr_for_test();
let ms = MediaSource::open("testdata/exif.jpg").unwrap();
let iter = parser.parse_exif(ms).unwrap();
let _exif: crate::Exif = iter.into();
let ptr_after_second = parser.state.cached_ptr_for_test();
assert!(
ptr_after_first.is_some() && ptr_after_first == ptr_after_second,
"expected recycled allocation, got {:?} -> {:?}",
ptr_after_first,
ptr_after_second
);
}
#[test]
fn parser_new_does_no_upfront_allocation() {
let parser = MediaParser::new();
assert!(parser.state.cached_ptr_for_test().is_none());
assert!(parser.state.buf_is_none_for_test());
}
#[test]
fn buffered_state_memory_mode_sets_and_reads() {
let mut s = BufferedParserState::new();
s.set_memory(bytes::Bytes::from_static(b"abcdefgh"));
assert!(s.is_memory_mode());
assert_eq!(s.buffer(), b"abcdefgh");
s.set_position(3);
assert_eq!(s.buffer(), b"defgh");
}
#[test]
fn buffered_state_share_buf_memory_mode_is_zero_copy() {
let original = bytes::Bytes::from_static(b"the parser owns nothing here");
let original_ptr = original.as_ptr();
let mut s = BufferedParserState::new();
s.set_memory(original);
let (shared, position) = s.share_buf();
assert_eq!(position, 0);
assert_eq!(
shared.as_ptr(),
original_ptr,
"memory share must be a Bytes::clone, not a Vec round-trip"
);
assert!(!s.is_memory_mode());
}
#[test]
fn buffered_state_reset_clears_memory() {
let mut s = BufferedParserState::new();
s.set_memory(bytes::Bytes::from_static(b"x"));
s.reset();
assert!(!s.is_memory_mode());
assert_eq!(s.position, 0);
}
#[test]
fn buffered_state_acquire_buf_skips_in_memory_mode() {
let mut s = BufferedParserState::new();
s.set_memory(bytes::Bytes::from_static(b"data"));
s.acquire_buf();
assert!(s.buf.is_none());
assert_eq!(s.buffer(), b"data");
}
#[test]
fn media_source_from_bytes_image_jpg() {
let raw = std::fs::read("testdata/exif.jpg").unwrap();
let ms = MediaSource::from_bytes(raw).unwrap();
assert_eq!(ms.kind(), MediaKind::Image);
assert!(ms.memory.is_some());
}
#[test]
fn media_source_from_bytes_track_mov() {
let raw = std::fs::read("testdata/meta.mov").unwrap();
let ms = MediaSource::from_bytes(raw).unwrap();
assert_eq!(ms.kind(), MediaKind::Track);
}
#[test]
fn media_source_from_bytes_static_slice() {
let raw: &'static [u8] = include_bytes!("../testdata/exif.jpg");
let ms = MediaSource::from_bytes(raw).unwrap();
assert_eq!(ms.kind(), MediaKind::Image);
}
#[test]
fn media_source_from_bytes_rejects_too_short() {
let raw = vec![0u8; 4];
let res = MediaSource::from_bytes(raw);
assert!(res.is_err(), "expected mime-detection error");
}
#[test]
fn media_source_from_bytes_rejects_unknown_mime() {
let raw = vec![0xAAu8; 256];
let res = MediaSource::from_bytes(raw);
assert!(
res.is_err(),
"expected mime-detection error for unknown bytes"
);
}
#[test]
fn p4_5_baseline_exif_jpg_full_dump() {
let mut parser = MediaParser::new();
let ms = MediaSource::open("testdata/exif.jpg").unwrap();
let iter: ExifIter = parser.parse_exif(ms).unwrap();
let mut entries: Vec<String> = iter
.map(|e| {
let tag_name = match e.tag() {
crate::TagOrCode::Tag(t) => format!("{t}"),
crate::TagOrCode::Unknown(c) => format!("0x{c:04x}"),
};
let value_str = e
.value()
.map(|v| format!("{v}"))
.unwrap_or_else(|| "<err>".into());
format!("{}.{}={:?}", e.ifd(), tag_name, value_str)
})
.collect();
entries.sort();
let snapshot = entries.join("\n");
assert!(
entries.len() > 5,
"expected >5 entries, got {}",
entries.len()
);
assert!(snapshot.contains("Make"), "expected Make tag in snapshot");
}
#[test]
fn parse_exif_from_bytes_jpg_basic() {
let mut parser = MediaParser::new();
let raw = std::fs::read("testdata/exif.jpg").unwrap();
let ms = MediaSource::from_bytes(raw).unwrap();
let iter = parser.parse_exif_from_bytes(ms).unwrap();
let exif: crate::Exif = iter.into();
assert!(exif.get(crate::ExifTag::Make).is_some());
}
#[test]
fn parse_exif_from_bytes_heic_basic() {
let mut parser = MediaParser::new();
let raw = std::fs::read("testdata/exif.heic").unwrap();
let ms = MediaSource::from_bytes(raw).unwrap();
let iter = parser.parse_exif_from_bytes(ms).unwrap();
let exif: crate::Exif = iter.into();
assert_eq!(
exif.get(crate::ExifTag::Make).and_then(|v| v.as_str()),
Some("Apple")
);
}
#[test]
fn parse_exif_from_bytes_zero_copy_shared_bytes() {
let raw = std::fs::read("testdata/exif.jpg").unwrap();
let bytes = bytes::Bytes::from(raw);
let original_ptr = bytes.as_ptr();
let mut parser = MediaParser::new();
let ms = MediaSource::from_bytes(bytes).unwrap();
let iter = parser.parse_exif_from_bytes(ms).unwrap();
assert!(
parser.state.cached_ptr_for_test().is_none(),
"memory mode must not poison the recycle cache"
);
drop(iter);
let bytes2 = bytes::Bytes::from(std::fs::read("testdata/exif.jpg").unwrap());
let ms2 = MediaSource::from_bytes(bytes2.clone()).unwrap();
let _iter2 = parser.parse_exif_from_bytes(ms2).unwrap();
let _ = original_ptr; }
#[test]
fn parse_exif_from_bytes_on_track_returns_exif_not_found() {
let mut parser = MediaParser::new();
let raw = std::fs::read("testdata/meta.mov").unwrap();
let ms = MediaSource::from_bytes(raw).unwrap();
let res = parser.parse_exif_from_bytes(ms);
assert!(matches!(res, Err(crate::Error::ExifNotFound)));
}
#[test]
fn parse_exif_from_bytes_on_truncated_returns_io_error() {
let mut raw = std::fs::read("testdata/exif.jpg").unwrap();
raw.truncate(200);
let mut parser = MediaParser::new();
let ms = MediaSource::from_bytes(raw).unwrap();
let res = parser.parse_exif_from_bytes(ms);
assert!(
res.is_err(),
"expected error on truncated bytes, got {:?}",
res
);
}
#[test]
fn parse_track_from_bytes_mov_basic() {
let mut parser = MediaParser::new();
let raw = std::fs::read("testdata/meta.mov").unwrap();
let ms = MediaSource::from_bytes(raw).unwrap();
let info = parser.parse_track_from_bytes(ms).unwrap();
assert_eq!(info.get(crate::TrackInfoTag::Make), Some(&"Apple".into()));
assert_eq!(
info.get(crate::TrackInfoTag::Model),
Some(&"iPhone X".into())
);
}
#[test]
fn parse_track_from_bytes_mp4_basic() {
let mut parser = MediaParser::new();
let raw = std::fs::read("testdata/meta.mp4").unwrap();
let ms = MediaSource::from_bytes(raw).unwrap();
let info = parser.parse_track_from_bytes(ms).unwrap();
assert!(info.get(crate::TrackInfoTag::CreateDate).is_some());
}
#[test]
fn parse_track_from_bytes_mkv_basic() {
let mut parser = MediaParser::new();
let raw = std::fs::read("testdata/mkv_640x360.mkv").unwrap();
let ms = MediaSource::from_bytes(raw).unwrap();
let info = parser.parse_track_from_bytes(ms).unwrap();
assert_eq!(
info.get(crate::TrackInfoTag::Width),
Some(&(640_u32.into()))
);
}
#[test]
fn parse_track_from_bytes_on_image_returns_track_not_found() {
let mut parser = MediaParser::new();
let raw = std::fs::read("testdata/exif.jpg").unwrap();
let ms = MediaSource::from_bytes(raw).unwrap();
let res = parser.parse_track_from_bytes(ms);
assert!(matches!(res, Err(crate::Error::TrackNotFound)));
}
}