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", b"heix", b"hevc", b"heim", b"heis", b"hevm", b"hevs", b"mif1", b"MiHE", b"miaf", b"MiHB", ];
27
28const HEIC_BRAND_NAMES: &[&[u8]] = &[b"heic", b"heix", b"heim", b"heis"];
29
30const 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(since = "2.0.0")]
102#[allow(unused)]
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum FileFormat {
105 Jpeg,
106 Heif,
108
109 QuickTime,
118 MP4,
119
120 Ebml,
122}
123
124#[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 return Ok(Mime::Video(MimeVideo::QuickTime));
189 }
190
191 return Err(crate::Error::UnrecognizedFileFormat);
192 };
193
194 if QT_BRAND_NAMES.iter().any(|v| v.as_bytes() == major_brand) {
196 return Ok(Mime::Video(MimeVideo::QuickTime));
197 }
198
199 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 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 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 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 return Ok(FileFormat::QuickTime);
264 }
265
266 return Err(crate::Error::UnrecognizedFileFormat);
267 };
268
269 if QT_BRAND_NAMES.iter().any(|v| v.as_bytes() == major_brand) {
271 return Ok(FileFormat::QuickTime);
272 }
273
274 if HEIF_HEIC_BRAND_NAMES.contains(&major_brand) {
276 return Ok(FileFormat::Heif);
277 }
278
279 if MP4_BRAND_NAMES.iter().any(|v| v.as_bytes() == major_brand) {
281 return Ok(FileFormat::MP4);
282 }
283
284 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 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 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}