Skip to main content

nom_exif/
file.rs

1use nom::{bytes::complete, multi::many0, FindSubstring};
2use std::{
3    fmt::Display,
4    io::{Cursor, Read},
5};
6
7use crate::{
8    bbox::{travel_header, BoxHolder},
9    ebml::element::parse_ebml_doc_type,
10    error::{ParsedError, ParsingError},
11    exif::TiffHeader,
12    jpeg::check_jpeg,
13    loader::Load,
14    slice::SubsliceRange,
15};
16
17const HEIF_HEIC_BRAND_NAMES: &[&[u8]] = &[
18    b"heic", // the usual HEIF images
19    b"heix", // 10bit images, or anything that uses h265 with range extension
20    b"hevc", // 'hevx': brands for image sequences
21    b"heim", // multiview
22    b"heis", // scalable
23    b"hevm", // multiview sequence
24    b"hevs", // scalable sequence
25    b"mif1", b"MiHE", b"miaf", b"MiHB", // HEIC file's compatible brands
26];
27
28const HEIC_BRAND_NAMES: &[&[u8]] = &[b"heic", b"heix", b"heim", b"heis"];
29
30// TODO: Refer to the information on the website https://www.ftyps.com to add
31// other less common MP4 brands.
32const MP4_BRAND_NAMES: &[&str] = &[
33    "3g2a", "3g2b", "3g2c", "3ge6", "3ge7", "3gg6", "3gp4", "3gp5", "3gp6", "3gs7", "avc1", "mp41",
34    "mp42", "iso2", "isom", "vfj1",
35];
36
37const QT_BRAND_NAMES: &[&str] = &["qt  ", "mqt "];
38
39#[derive(Debug, Clone, PartialEq, Eq, Copy)]
40pub(crate) enum Mime {
41    Image(MimeImage),
42    Video(MimeVideo),
43}
44
45impl Mime {
46    pub fn unwrap_image(self) -> MimeImage {
47        match self {
48            Mime::Image(val) => val,
49            Mime::Video(_) => panic!("called `Mime::unwrap_image()` on an `Mime::Video`"),
50        }
51    }
52    pub fn unwrap_video(self) -> MimeVideo {
53        match self {
54            Mime::Image(_) => panic!("called `Mime::unwrap_video()` on an `Mime::Image`"),
55            Mime::Video(val) => val,
56        }
57    }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Copy)]
61pub(crate) enum MimeImage {
62    Jpeg,
63    Heic,
64    Heif,
65    Tiff,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Copy)]
69pub(crate) enum MimeVideo {
70    QuickTime,
71    Mp4,
72    Webm,
73    Matroska,
74    _3gpp,
75}
76
77impl TryFrom<&[u8]> for Mime {
78    type Error = crate::Error;
79    fn try_from(input: &[u8]) -> Result<Self, Self::Error> {
80        let mime = if let Ok(x) = parse_bmff_mime(input) {
81            x
82        } else if let Ok(x) = get_ebml_doc_type(input) {
83            if x == "webm" {
84                Mime::Video(MimeVideo::Webm)
85            } else {
86                Mime::Video(MimeVideo::Matroska)
87            }
88        } else if TiffHeader::parse(input).is_ok() {
89            Mime::Image(MimeImage::Tiff)
90        } else if check_jpeg(input).is_ok() {
91            Mime::Image(MimeImage::Jpeg)
92        } else {
93            return Err(crate::Error::UnrecognizedFileFormat);
94        };
95
96        Ok(mime)
97    }
98}
99
100/// *Deprecated*: Please use [`MediaSource`] instead.
101#[deprecated(since = "2.0.0")]
102#[allow(unused)]
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum FileFormat {
105    Jpeg,
106    /// heic, heif
107    Heif,
108
109    // Currently, there is not much difference between QuickTime and MP4 when
110    // parsing metadata, and they share the same parsing mechanism.
111    //
112    // The only difference is that if detected as an MP4 file, the
113    // `moov/udta/©xyz` atom is additionally checked and an attempt is made to
114    // read GPS information from it, since Android phones store GPS information
115    // in that atom.
116    /// mov
117    QuickTime,
118    MP4,
119
120    /// webm, mkv, mka, mk3d
121    Ebml,
122}
123
124// Parse the input buffer and detect its file type
125#[allow(deprecated)]
126impl TryFrom<&[u8]> for FileFormat {
127    type Error = crate::Error;
128
129    fn try_from(input: &[u8]) -> Result<Self, Self::Error> {
130        if let Ok(ff) = check_bmff(input) {
131            Ok(ff)
132        } else if get_ebml_doc_type(input).is_ok() {
133            Ok(Self::Ebml)
134        } else if check_jpeg(input).is_ok() {
135            Ok(Self::Jpeg)
136        } else {
137            Err(crate::Error::UnrecognizedFileFormat)
138        }
139    }
140}
141
142#[allow(deprecated)]
143impl FileFormat {
144    pub fn try_from_read<T: Read>(reader: T) -> crate::Result<Self> {
145        const BUF_SIZE: usize = 4096;
146        let mut buf = Vec::with_capacity(BUF_SIZE);
147        let n = reader.take(BUF_SIZE as u64).read_to_end(buf.as_mut())?;
148        if n == 0 {
149            Err("file is empty")?;
150        }
151
152        buf.as_slice().try_into()
153    }
154
155    pub(crate) fn try_from_load<T: Load>(loader: &mut T) -> Result<Self, ParsedError> {
156        loader.load_and_parse(|x| {
157            x.try_into()
158                .map_err(|_| ParsingError::Failed("unrecognized file format".to_string()))
159        })
160    }
161}
162
163#[allow(deprecated)]
164impl Display for FileFormat {
165    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166        match self {
167            Self::Jpeg => "JPEG".fmt(f),
168            Self::Heif => "HEIF/HEIC".fmt(f),
169            Self::QuickTime => "QuickTime".fmt(f),
170            Self::MP4 => "MP4".fmt(f),
171            Self::Ebml => "EBML".fmt(f),
172        }
173    }
174}
175
176fn get_ebml_doc_type(input: &[u8]) -> crate::Result<String> {
177    let mut cursor = Cursor::new(input);
178    let doc = parse_ebml_doc_type(&mut cursor)?;
179    Ok(doc)
180}
181
182fn parse_bmff_mime(input: &[u8]) -> crate::Result<Mime> {
183    let (ftyp, Some(major_brand)) =
184        get_ftyp_and_major_brand(input).map_err(|_| crate::Error::UnrecognizedFileFormat)?
185    else {
186        if travel_header(input, |header, _| header.box_type != "mdat").is_ok() {
187            // ftyp is None, mdat box is found, assume it's a MOV file extracted from HEIC
188            return Ok(Mime::Video(MimeVideo::QuickTime));
189        }
190
191        return Err(crate::Error::UnrecognizedFileFormat);
192    };
193
194    // Check if it is a QuickTime file
195    if QT_BRAND_NAMES.iter().any(|v| v.as_bytes() == major_brand) {
196        return Ok(Mime::Video(MimeVideo::QuickTime));
197    }
198
199    // Check if it is a HEIF file
200    if HEIF_HEIC_BRAND_NAMES.contains(&major_brand) {
201        if HEIC_BRAND_NAMES.contains(&major_brand) {
202            return Ok(Mime::Image(MimeImage::Heic));
203        }
204        return Ok(Mime::Image(MimeImage::Heif));
205    }
206
207    // Check if it is a MP4 file
208    if MP4_BRAND_NAMES.iter().any(|v| v.as_bytes() == major_brand) {
209        if major_brand.starts_with(b"3gp") {
210            return Ok(Mime::Video(MimeVideo::_3gpp));
211        }
212        return Ok(Mime::Video(MimeVideo::Mp4));
213    }
214
215    // Check compatible brands
216    let compatible_brands = ftyp.body_data();
217
218    if QT_BRAND_NAMES
219        .iter()
220        .any(|v| compatible_brands.find_substring(v.as_bytes()).is_some())
221    {
222        return Ok(Mime::Video(MimeVideo::QuickTime));
223    }
224
225    if HEIF_HEIC_BRAND_NAMES
226        .iter()
227        .any(|x| compatible_brands.find_substring(*x).is_some())
228    {
229        if HEIC_BRAND_NAMES.contains(&major_brand) {
230            return Ok(Mime::Image(MimeImage::Heic));
231        }
232        return Ok(Mime::Image(MimeImage::Heif));
233    }
234
235    if MP4_BRAND_NAMES
236        .iter()
237        .any(|v| compatible_brands.subslice_in_range(v.as_bytes()).is_some())
238    {
239        if major_brand.starts_with(b"3gp") {
240            return Ok(Mime::Video(MimeVideo::_3gpp));
241        }
242        return Ok(Mime::Video(MimeVideo::Mp4));
243    }
244
245    tracing::warn!(
246        marjor_brand = major_brand.iter().map(|b| *b as char).collect::<String>(),
247        "unknown major brand",
248    );
249
250    if travel_header(input, |header, _| header.box_type != "mdat").is_ok() {
251        // mdat box found, assume it's a mp4 file
252        return Ok(Mime::Video(MimeVideo::Mp4));
253    }
254
255    Err(crate::Error::UnrecognizedFileFormat)
256}
257
258#[allow(deprecated)]
259fn check_bmff(input: &[u8]) -> crate::Result<FileFormat> {
260    let (ftyp, Some(major_brand)) = get_ftyp_and_major_brand(input)? else {
261        if travel_header(input, |header, _| header.box_type != "mdat").is_ok() {
262            // ftyp is None, mdat box is found, assume it's a MOV file extracted from HEIC
263            return Ok(FileFormat::QuickTime);
264        }
265
266        return Err(crate::Error::UnrecognizedFileFormat);
267    };
268
269    // Check if it is a QuickTime file
270    if QT_BRAND_NAMES.iter().any(|v| v.as_bytes() == major_brand) {
271        return Ok(FileFormat::QuickTime);
272    }
273
274    // Check if it is a HEIF file
275    if HEIF_HEIC_BRAND_NAMES.contains(&major_brand) {
276        return Ok(FileFormat::Heif);
277    }
278
279    // Check if it is a MP4 file
280    if MP4_BRAND_NAMES.iter().any(|v| v.as_bytes() == major_brand) {
281        return Ok(FileFormat::MP4);
282    }
283
284    // Check compatible brands
285    let compatible_brands = get_compatible_brands(ftyp.body_data())?;
286
287    if QT_BRAND_NAMES
288        .iter()
289        .any(|v| compatible_brands.iter().any(|x| v.as_bytes() == *x))
290    {
291        return Ok(FileFormat::QuickTime);
292    }
293
294    if HEIF_HEIC_BRAND_NAMES
295        .iter()
296        .any(|x| compatible_brands.contains(x))
297    {
298        return Ok(FileFormat::Heif);
299    }
300
301    if MP4_BRAND_NAMES
302        .iter()
303        .any(|v| compatible_brands.iter().any(|x| v.as_bytes() == *x))
304    {
305        return Ok(FileFormat::MP4);
306    }
307
308    tracing::warn!(
309        marjor_brand = major_brand.iter().map(|b| *b as char).collect::<String>(),
310        "unknown major brand",
311    );
312
313    if travel_header(input, |header, _| header.box_type != "mdat").is_ok() {
314        // find mdat box, assume it's a mp4 file
315        return Ok(FileFormat::MP4);
316    }
317
318    Err(crate::Error::UnrecognizedFileFormat)
319}
320
321fn get_ftyp_and_major_brand(input: &[u8]) -> crate::Result<(BoxHolder, Option<&[u8]>)> {
322    let (_, bbox) = BoxHolder::parse(input).map_err(|e| format!("parse ftyp failed: {e}"))?;
323
324    if bbox.box_type() == "ftyp" {
325        if bbox.body_data().len() < 4 {
326            return Err(format!(
327                "parse ftyp failed; body size should greater than 4, got {}",
328                bbox.body_data().len()
329            )
330            .into());
331        }
332        let (_, ftyp) = complete::take(4_usize)(bbox.body_data())?;
333        Ok((bbox, Some(ftyp)))
334    } else if bbox.box_type() == "wide" {
335        // MOV files that extracted from HEIC starts with `wide` & `mdat` atoms
336        Ok((bbox, None))
337    } else {
338        Err(format!("parse ftyp failed; first box type is: {}", bbox.box_type()).into())
339    }
340}
341
342fn get_compatible_brands(body: &[u8]) -> crate::Result<Vec<&[u8]>> {
343    let Ok((_, brands)) = many0(complete::take::<usize, &[u8], nom::error::Error<&[u8]>>(
344        4_usize,
345    ))(body) else {
346        return Err("get compatible brands failed".into());
347    };
348    Ok(brands)
349}
350
351#[allow(deprecated)]
352#[cfg(test)]
353mod tests {
354    use std::ops::Deref;
355
356    use super::*;
357    use test_case::test_case;
358    use Mime::*;
359    use MimeImage::*;
360    use MimeVideo::*;
361
362    use crate::testkit::{open_sample, read_sample};
363
364    #[test_case("exif.heic", Image(Heic))]
365    #[test_case("exif.jpg", Image(Jpeg))]
366    #[test_case("meta.mp4", Video(Mp4))]
367    #[test_case("meta.mov", Video(QuickTime))]
368    #[test_case("embedded-in-heic.mov", Video(QuickTime))]
369    #[test_case("compatible-brands.mov", Video(QuickTime))]
370    #[test_case("webm_480.webm", Video(Webm))]
371    #[test_case("mkv_640x360.mkv", Video(Matroska))]
372    #[test_case("mka.mka", Video(Matroska))]
373    #[test_case("3gp_640x360.3gp", Video(_3gpp))]
374    fn mime(path: &str, mime: Mime) {
375        let data = read_sample(path).unwrap();
376        let m: Mime = data.deref().try_into().unwrap();
377        assert_eq!(m, mime);
378    }
379
380    #[test_case("exif.heic", FileFormat::Heif)]
381    #[test_case("exif.jpg", FileFormat::Jpeg)]
382    #[test_case("meta.mov", FileFormat::QuickTime)]
383    #[test_case("meta.mp4", FileFormat::MP4)]
384    #[test_case("embedded-in-heic.mov", FileFormat::QuickTime)]
385    #[test_case("compatible-brands.mov", FileFormat::QuickTime)]
386    fn file_format(path: &str, expect: FileFormat) {
387        let f = open_sample(path).unwrap();
388        let ff = FileFormat::try_from_read(f).unwrap();
389        assert_eq!(ff, expect);
390    }
391
392    #[test_case("compatible-brands-fail.mov")]
393    fn file_format_error(path: &str) {
394        let f = open_sample(path).unwrap();
395        FileFormat::try_from_read(f).unwrap_err();
396    }
397}