nom-exif-2.5.4/.cargo_vcs_info.json0000644000000001360000000000100125660ustar { "git": { "sha1": "1dc6eeb728d72b7fbe8d85e9af6991161da48769" }, "path_in_vcs": "" }nom-exif-2.5.4/.github/workflows/rust.yml000064400000000000000000000011531046102023000164730ustar 00000000000000name: Rust on: push: branches: [ "main" ] pull_request: branches: [ "main" ] env: CARGO_TERM_COLOR: always jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Build run: cargo build --verbose - name: Add Android targets run: rustup target add armv7-linux-androideabi - name: Build for 32-bit target run: cargo build --target armv7-linux-androideabi --verbose - name: Run tests run: cargo test --verbose -- --nocapture - name: Run tests for all features run: cargo test --all-features --verbose -- --nocapture nom-exif-2.5.4/.gitignore000064400000000000000000000007441046102023000133530ustar 00000000000000# Generated by Cargo # will have compiled files and executables debug/ target/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk # MSVC Windows builds of rustc generate these, which store debugging information *.pdb .DS_Store /testdata/*.html /debug.log /testdata/fujifilm_x_t1_01.raf nom-exif-2.5.4/CHANGELOG.md000064400000000000000000000156351046102023000132010ustar 00000000000000# Changelog ## nom-exif v2.5.4 ### Fixed - Fixed fuzzing-induced hangs. ## nom-exif v2.5.3 ### Fixed - Fixed fuzzing-induced crashes. ## nom-exif v2.5.2 ### Fixed - IFD parse error for large "MakerNote" entries (TIFF/IIQ source) #40 ## nom-exif v2.5.1 ### Fixed - Panic when parsing GPSInfo #37 ## nom-exif v2.5.0 ### Added - `EntryValue::NaiveDateTime` #38 ### Fixed - Try to repair broken `OffsetTimeOriginal`/`OffsetTime`, if it's not repairable, then parse time as an `NaiveDateTime`. #39 ## nom-exif v2.4.3 ### Fixed - Ignore undetermined language flag (0x55c4) when parsing `TrackInfoTag::Author` info #36 ## nom-exif v2.4.2 ### Fixed - Fix: panic: skip a large number of bytes #28 #26 - Fix: panic when parsing moov/trak #29 ## nom-exif v2.4.1 ### Fixed - Fix: Panic for Nikon D200 raw NEF file (parse_ifd_entry_header) #34 ## nom-exif v2.4.0 ### Added - Add `TrackInfoTag::Author` #36 - Parse `udta/auth` or `com.apple.quicktime.author` info from `mp4`/`mov` files ## nom-exif v2.3.1 [v2.3.0..v2.3.1](https://github.com/mindeng/nom-exif/compare/v2.3.0..v2.3.1) ### Fixed - parse GPS strings correctly #35 ## nom-exif v2.3.0 [v2.2.1..v2.3.0](https://github.com/mindeng/nom-exif/compare/v2.2.1..v2.3.0) ### Added - `EntryValue::U8Array` ### Fixed - Doesn't recognize DateTimeOriginal tag in file. #33 - Update to avoid Rust 1.83 lints #30 - println! prints lines to output #27 - range start index panic in src/exif/exif_iter.rs #25 - Panic range end index 131151 out of range for slice of length 129 #24 - Memory allocation failed when decoding invalid file #22 - assertion failed: data.len() >= 6 when checking broken exif file #21 - Panic depth shouldn't be greater than 1 #20 - freeze when checking broken file #19 ## nom-exif v2.2.1 [v2.1.1..v2.2.1](https://github.com/mindeng/nom-exif/compare/v2.1.1..v2.2.1) ### Added - Added support for RAF (Fujifilm RAW) file type. ## nom-exif v2.1.1 [v2.1.0..v2.1.1](https://github.com/mindeng/nom-exif/compare/v2.1.0..v2.1.1) ### Fixed - Fix endless loop caused by some broken images. ## nom-exif v2.1.0 [v2.0.2..v2.1.0](https://github.com/mindeng/nom-exif/compare/v2.0.2..v2.1.0) ### Added - Type alias: `IRational` - Supports 32-bit target platforms, e.g.: `armv7-linux-androideabi` ### Fixed - Fix compiling errors on 32-bit target platforms ## nom-exif v2.0.2 [v2.0.0..v2.0.2](https://github.com/mindeng/nom-exif/compare/v2.0.0..v2.0.2) ### Changed - Deprecated - `parse_mov_metadata`: Please use `MediaParser` instead. ## nom-exif v2.0.0 [v1.5.2..v2.0.0](https://github.com/mindeng/nom-exif/compare/v1.5.2..v2.0.0) ### Added - Support more file types - `*.tiff` - `*.webm` - `*.mkv`, `*.mka` - `*.3gp` - rexiftool - Add `--debug` command line parameter for printing and saving debug logs - Structs - `MediaSource` - `MediaParser` - `AsyncMediaSource` - `AsyncMediaParser` - `TrackInfo` - Enums - `TrackInfoTag` - Type Aliases - `URational` ### Changed - Deprecated - `parse_exif` : Please use `MediaParser` instead. - `parse_exif_async` : Please use `MediaParser` instead. - `parse_heif_exif` : Please use `MediaParser` instead. - `parse_jpeg_exif` : Please use `MediaParser` instead. - `parse_metadata` : Please use `MediaParser` instead. - `FileFormat` : Please use `MediaSource` instead. ## nom-exif v1.5.2 [v1.5.1..v1.5.2](https://github.com/mindeng/nom-exif/compare/v1.5.1..v1.5.2) ### Fixed - Bug fixed: "Box is too big" error when parsing some mov/mp4 files No need to limit box body size when parsing/traveling box headers, only need to do that limitation when parsing box body (this restriction is necessary for the robustness of the program). Additionally, I also changed the size limit on the box body to a more reasonable value. ## nom-exif v1.5.1 [v1.5.0..v1.5.1](https://github.com/mindeng/nom-exif/compare/v1.5.0..v1.5.1) ### Added - `ParsedExifEntry` ### Changed - `ExifTag::Unknown` ## nom-exif v1.5.0 [v1.4.1..v1.5.0](https://github.com/mindeng/nom-exif/compare/v1.4.1..v1.5.0) ### Added - `parse_exif` - `parse_exif_async` - `ExifIter` - `GPSInfo` - `LatLng` - `FileFormat` - `Exif::get` - `Exif::get_by_tag_code` - `EntryValue::URationalArray` - `EntryValue::IRationalArray` - `Error::InvalidEntry` - `Error::EntryHasBeenTaken` ### Changed - `Exif::get_values` deprecated - `Exif::get_value` deprecated - `Exif::get_value_by_tag_code` deprecated - `Error::NotFound` deprecated ## nom-exif v1.4.1 [v1.4.0..v1.4.1](https://github.com/mindeng/nom-exif/compare/v1.4.0..v1.4.1) ### Performance Improved - Avoid data copying when extracting moov body. ### Added - impl `Send` + `Sync` for `Exif`, so we can use it in multi-thread environment ## nom-exif v1.4.0 [v1.3.0..v1.4.0](https://github.com/mindeng/nom-exif/compare/v1.3.0..v1.4.0) ### Performance Improved - Avoid data copying during parsing IFD entries. ## nom-exif v1.3.0 [v1.2.6..v1.3.0](https://github.com/mindeng/nom-exif/compare/v1.2.6..v1.3.0) ### Changed - Introduce tracing, and replace printing with tracing. ## nom-exif v1.2.6 [v1.2.5..v1.2.6](https://github.com/mindeng/nom-exif/compare/v1.2.5..v1.2.6) ### Fixed - Bug fixed: [A broken JPEG file - Library cannot read it, that exiftool reads properly #2](https://github.com/mindeng/nom-exif/issues/2) - Bug fixed: [Another Unsupported MP4 file #7](https://github.com/mindeng/nom-exif/issues/7#issuecomment-2226853761) ### Internal - Remove redundant `fn open_sample` definitions in test cases. - Use `read_sample` instead of `open_sample` when possible. ## nom-exif v1.2.5 [v1.2.4..v1.2.5](https://github.com/mindeng/nom-exif/compare/v1.2.4..v1.2.5) ### Fixed - Bug fixed: [Unsupported mov file? #7](https://github.com/mindeng/nom-exif/issues/7) ### Internal - Change `travel_while` to return a result of optional `BoxHolder`, so we can distinguish whether it is a parsing error or just not found. ## nom-exif v1.2.4 [8c00f1b..v1.2.4](https://github.com/mindeng/nom-exif/compare/8c00f1b..v1.2.4) ### Improved - **Compatibility** has been greatly improved: compatible brands in ftyp box has been checked, and now it can support various compatible MP4/MOV files. ## nom-exif v1.2.3 [2861cbc..8c00f1b](https://github.com/mindeng/nom-exif/compare/2861cbc..8c00f1b) ### Fixed - **All** clippy warnings has been fixed! ### Changed - **Deprecated** some less commonly used APIs and introduced several new ones, mainly to satisfy clippy requirements, e.g.: - `GPSInfo.to_iso6709` -> `format_iso6709` - `URational.to_float` -> `as_float` See commit [8c5dc26](https://github.com/mindeng/nom-exif/commit/8c5dc26). ## nom-exif v1.2.2 [9b7fdf7..2861cbc](https://github.com/mindeng/nom-exif/compare/9b7fdf7..2861cbc) ### Added - **Fuzz testing**: Added *afl-fuzz* for fuzz testing. ### Changed ### Fixed - **Robustness improved**: Fixed all crash issues discovered during fuzz testing. - **Clippy warnings**: Checked with the latest clippy and fixed almost all of the warnings. nom-exif-2.5.4/Cargo.lock0000644000000660150000000000100105510ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "addr2line" version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] name = "adler2" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "android-tzdata" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "anstream" version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" dependencies = [ "windows-sys", ] [[package]] name = "anstyle-wincon" version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" dependencies = [ "anstyle", "once_cell_polyfill", "windows-sys", ] [[package]] name = "approx" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" dependencies = [ "num-traits", ] [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", "windows-targets", ] [[package]] name = "bitflags" version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "bumpalo" version = "3.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" [[package]] name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" version = "1.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac" dependencies = [ "shlex", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", "windows-link", ] [[package]] name = "clap" version = "4.5.39" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" dependencies = [ "clap_builder", "clap_derive", ] [[package]] name = "clap_builder" version = "4.5.39" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", ] [[package]] name = "clap_derive" version = "4.5.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" dependencies = [ "heck", "proc-macro2", "quote", "syn", ] [[package]] name = "clap_lex" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "colorchoice" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "geo-types" version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62ddb1950450d67efee2bbc5e429c68d052a822de3aad010d28b351fbb705224" dependencies = [ "approx", "num-traits", "serde", ] [[package]] name = "getrandom" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", "r-efi", "wasi", ] [[package]] name = "gimli" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "iana-time-zone" version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "log", "wasm-bindgen", "windows-core", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "iso6709parse" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5090db9c6a716d1f4eeb729957e889e9c28156061c825cbccd44950cf0f3c66" dependencies = [ "geo-types", "nom", ] [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ "once_cell", "wasm-bindgen", ] [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libm" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "log" version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "matchers" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" dependencies = [ "regex-automata 0.1.10", ] [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ "adler2", ] [[package]] name = "nom" version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", ] [[package]] name = "nom-exif" version = "2.5.4" dependencies = [ "bytes", "chrono", "clap", "iso6709parse", "nom", "rand", "regex", "serde", "serde_json", "test-case", "thiserror", "tokio", "tracing", "tracing-subscriber", ] [[package]] name = "nu-ansi-term" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" dependencies = [ "overload", "winapi", ] [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", "libm", ] [[package]] name = "object" version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "overload" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "ppv-lite86" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] [[package]] name = "proc-macro2" version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" [[package]] name = "rand" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" dependencies = [ "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ "getrandom", ] [[package]] name = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", "regex-automata 0.4.9", "regex-syntax 0.8.5", ] [[package]] name = "regex-automata" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" dependencies = [ "regex-syntax 0.6.29", ] [[package]] name = "regex-automata" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", "regex-syntax 0.8.5", ] [[package]] name = "regex-syntax" version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustversion" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" [[package]] name = "ryu" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "serde" version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", "memchr", "ryu", "serde", ] [[package]] name = "sharded-slab" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ "lazy_static", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "test-case" version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8" dependencies = [ "test-case-macros", ] [[package]] name = "test-case-core" version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f" dependencies = [ "cfg-if", "proc-macro2", "quote", "syn", ] [[package]] name = "test-case-macros" version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" dependencies = [ "proc-macro2", "quote", "syn", "test-case-core", ] [[package]] name = "thiserror" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "thread_local" version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ "cfg-if", "once_cell", ] [[package]] name = "tokio" version = "1.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" dependencies = [ "backtrace", "bytes", "pin-project-lite", "tokio-macros", ] [[package]] name = "tokio-macros" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tracing" version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tracing-core" version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", "valuable", ] [[package]] name = "tracing-log" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ "log", "once_cell", "tracing-core", ] [[package]] name = "tracing-subscriber" version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ "matchers", "nu-ansi-term", "once_cell", "regex", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", ] [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "valuable" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "wasi" version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ "wit-bindgen-rt", ] [[package]] name = "wasm-bindgen" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" dependencies = [ "unicode-ident", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", "windows-link", "windows-result", "windows-strings", ] [[package]] name = "windows-implement" version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "windows-interface" version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "windows-link" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" [[package]] name = "windows-result" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ "windows-link", ] [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "wit-bindgen-rt" version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ "bitflags", ] [[package]] name = "zerocopy" version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" dependencies = [ "proc-macro2", "quote", "syn", ] nom-exif-2.5.4/Cargo.toml0000644000000043730000000000100105730ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.80" name = "nom-exif" version = "2.5.4" build = false exclude = ["testdata/*"] autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Exif/metadata parsing library written in pure Rust, both image (jpeg/heif/heic/jpg/tiff etc.) and video/audio (mov/mp4/3gp/webm/mkv/mka, etc.) files are supported." homepage = "https://github.com/mindeng/nom-exif" readme = "README.md" keywords = [ "metadata", "exif", ] categories = [ "multimedia::images", "multimedia::video", "multimedia::audio", "parsing", "parser-implementations", ] license-file = "LICENSE" repository = "https://github.com/mindeng/nom-exif" [features] async = ["tokio"] json_dump = ["serde"] [lib] name = "nom_exif" path = "src/lib.rs" [[example]] name = "rexiftool" path = "examples/rexiftool.rs" [dependencies.bytes] version = "1.7.1" [dependencies.chrono] version = "0.4" [dependencies.iso6709parse] version = "0.1.0" [dependencies.nom] version = "7.1" [dependencies.regex] version = "1.10" [dependencies.serde] version = "1.0" features = ["derive"] optional = true [dependencies.thiserror] version = "2.0.11" [dependencies.tokio] version = "1.40.0" features = [ "fs", "io-util", ] optional = true [dependencies.tracing] version = "0.1.40" [dev-dependencies.chrono] version = "0.4" [dev-dependencies.clap] version = "4.4" features = ["derive"] [dev-dependencies.rand] version = "0.9" [dev-dependencies.regex] version = "1.10" [dev-dependencies.serde_json] version = "1.0" [dev-dependencies.test-case] version = "3" [dev-dependencies.tokio] version = "1.40.0" features = [ "rt-multi-thread", "macros", "fs", "io-util", ] [dev-dependencies.tracing-subscriber] version = "0.3.18" features = ["env-filter"] nom-exif-2.5.4/Cargo.toml.orig000064400000000000000000000027521046102023000142530ustar 00000000000000[package] name = "nom-exif" rust-version = "1.80" version = "2.5.4" edition = "2021" license-file = "LICENSE" description = "Exif/metadata parsing library written in pure Rust, both image (jpeg/heif/heic/jpg/tiff etc.) and video/audio (mov/mp4/3gp/webm/mkv/mka, etc.) files are supported." homepage = "https://github.com/mindeng/nom-exif" repository = "https://github.com/mindeng/nom-exif" exclude = ["testdata/*"] categories = [ "multimedia::images", "multimedia::video", "multimedia::audio", "parsing", "parser-implementations", ] keywords = ["metadata", "exif"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] nom = "7.1" thiserror = "2.0.11" serde = { version = "1.0", features = ["derive"], optional = true } regex = { version = "1.10" } chrono = "0.4" tracing = { version = "0.1.40" } tokio = { version = "1.40.0", features = ["fs", "io-util"], optional = true } bytes = "1.7.1" iso6709parse = "0.1.0" [features] # default = ["async", "json_dump"] async = ["tokio"] json_dump = ["serde"] [dev-dependencies] test-case = "3" rand = "0.9" chrono = "0.4" serde_json = "1.0" regex = { version = "1.10" } clap = { version = "4.4", features = ["derive"] } tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } tokio = { version = "1.40.0", features = [ "rt-multi-thread", "macros", "fs", "io-util", ] } [[example]] name = "rexiftool" # required-features = ["json_dump"] [workspace] members = [".", "afl-fuzz"] nom-exif-2.5.4/LICENSE000064400000000000000000000020511046102023000123610ustar 00000000000000MIT License Copyright (c) 2024 Min Deng Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. nom-exif-2.5.4/README.md000064400000000000000000000233531046102023000126430ustar 00000000000000# Nom-Exif [![crates.io](https://img.shields.io/crates/v/nom-exif.svg)](https://crates.io/crates/nom-exif) [![Documentation](https://docs.rs/nom-exif/badge.svg)](https://docs.rs/nom-exif) [![LICENSE](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![CI](https://github.com/mindeng/nom-exif/actions/workflows/rust.yml/badge.svg)](https://github.com/mindeng/nom-exif/actions) `nom-exif` is an Exif/metadata parsing library written in pure Rust with [nom](https://github.com/rust-bakery/nom). ## Supported File Types - Image - *.heic, *.heif, etc. - *.jpg, *.jpeg - *.tiff, *.tif - *.RAF (Fujifilm RAW) - Video/Audio - ISO base media file format (ISOBMFF): *.mp4, *.mov, *.3gp, etc. - Matroska based file format: *.webm, *.mkv, *.mka, etc. ## Key Features - Ergonomic Design - **Unified Workflow** for Various File Types Now, multimedia files of different types and formats (including images, videos, and audio) can be processed using a unified method. This consistent API interface simplifies user experience and reduces cognitive load. The usage is demonstrated in the following examples. `examples/rexiftool` is also a good example. - Two style APIs for Exif *iterator* style ([`ExifIter`]) and *get* style ([`Exif`]). The former is parse-on-demand, and therefore, more detailed error information can be captured; the latter is simpler and easier to use. - Performance - *Zero-copy* when appropriate: Use borrowing and slicing instead of copying whenever possible. - Minimize I/O operations: When metadata is stored at the end/middle of a large file (such as a QuickTime file does), `Seek` rather than `Read` to quickly locate the location of the metadata (if the reader supports `Seek`). - Share I/O and parsing buffer between multiple parse calls: This can improve performance and avoid the overhead and memory fragmentation caused by frequent memory allocation. This feature is very useful when you need to perform batch parsing. - Pay as you go: When working with [`ExifIter`], all entries are lazy-parsed. That is, only when you iterate over [`ExifIter`] will the IFD entries be parsed one by one. - Robustness and stability Through long-term [Fuzz testing](https://github.com/rust-fuzz/afl.rs), and tons of crash issues discovered during testing have been fixed. Thanks to [@sigaloid](https://github.com/sigaloid) for [pointing this out](https://github.com/mindeng/nom-exif/pull/5)! - Supports both *sync* and *async* APIs ## Unified Workflow for Various File Types By using `MediaSource` & `MediaParser`, multimedia files of different types and formats (including images, videos, and audio) can be processed using a unified method. Here's an example: ```rust use nom_exif::*; fn main() -> Result<()> { let mut parser = MediaParser::new(); let files = [ "./testdata/exif.heic", "./testdata/exif.jpg", "./testdata/tif.tif", "./testdata/meta.mov", "./testdata/meta.mp4", "./testdata/webm_480.webm", "./testdata/mkv_640x360.mkv", "./testdata/mka.mka", "./testdata/3gp_640x360.3gp" ]; for f in files { let ms = MediaSource::file_path(f)?; if ms.has_exif() { // Parse the file as an Exif-compatible file let mut iter: ExifIter = parser.parse(ms)?; // ... } else if ms.has_track() { // Parse the file as a track let info: TrackInfo = parser.parse(ms)?; // ... } } Ok(()) } ``` ## Sync API: `MediaSource` + `MediaParser` `MediaSource` is an abstraction of multimedia data sources, which can be created from any object that implements the `Read` trait, and can be parsed by `MediaParser`. Example: ```rust use nom_exif::*; fn main() -> Result<()> { let mut parser = MediaParser::new(); let ms = MediaSource::file_path("./testdata/exif.heic")?; assert!(ms.has_exif()); let mut iter: ExifIter = parser.parse(ms)?; let exif: Exif = iter.into(); assert_eq!(exif.get(ExifTag::Make).unwrap().as_str().unwrap(), "Apple"); let ms = MediaSource::file_path("./testdata/meta.mov")?; assert!(ms.has_track()); let info: TrackInfo = parser.parse(ms)?; assert_eq!(info.get(TrackInfoTag::Make), Some(&"Apple".into())); assert_eq!(info.get(TrackInfoTag::Model), Some(&"iPhone X".into())); assert_eq!(info.get(TrackInfoTag::GpsIso6709), Some(&"+27.1281+100.2508+000.000/".into())); assert_eq!(info.get_gps_info().unwrap().latitude_ref, 'N'); assert_eq!( info.get_gps_info().unwrap().latitude, [(27, 1), (7, 1), (68, 100)].into(), ); // `MediaSource` can also be created from a `TcpStream`: // let ms = MediaSource::tcp_stream(stream)?; // Or from any `Read + Seek`: // let ms = MediaSource::seekable(stream)?; // From any `Read`: // let ms = MediaSource::unseekable(stream)?; Ok(()) } ``` See [`MediaSource`] & [`MediaParser`] for more information. ## Async API: `AsyncMediaSource` + `AsyncMediaParser` Likewise, `AsyncMediaParser` is an abstraction for asynchronous multimedia data sources, which can be created from any object that implements the `AsyncRead` trait, and can be parsed by `AsyncMediaParser`. Enable `async` feature flag for `nom-exif` in your `Cargo.toml`: ```toml [dependencies] nom-exif = { version = "1", features = ["async"] } ``` See [`AsyncMediaSource`] & [`AsyncMediaParser`] for more information. ## GPS Info `ExifIter` provides a convenience method for parsing gps information. (`Exif` & `TrackInfo` also provide a `get_gps_info` method). ```rust use nom_exif::*; fn main() -> Result<()> { let mut parser = MediaParser::new(); let ms = MediaSource::file_path("./testdata/exif.heic")?; let iter: ExifIter = parser.parse(ms)?; let gps_info = iter.parse_gps_info()?.unwrap(); assert_eq!(gps_info.format_iso6709(), "+43.29013+084.22713+1595.950CRSWGS_84/"); assert_eq!(gps_info.latitude_ref, 'N'); assert_eq!(gps_info.longitude_ref, 'E'); assert_eq!( gps_info.latitude, [(43, 1), (17, 1), (2446, 100)].into(), ); Ok(()) } ``` For more usage details, please refer to the [API documentation](https://docs.rs/nom-exif/latest/nom_exif/). ## CLI Tool `rexiftool` ### Human Readable Output `cargo run --example rexiftool testdata/meta.mov`: ``` text Make => Apple Model => iPhone X Software => 12.1.2 CreateDate => 2024-02-02T08:09:57+00:00 DurationMs => 500 ImageWidth => 720 ImageHeight => 1280 GpsIso6709 => +27.1281+100.2508+000.000/ ``` ### Json Dump `cargo run --example rexiftool testdata/meta.mov -j`: ``` text { "ImageWidth": "720", "Software": "12.1.2", "ImageHeight": "1280", "Make": "Apple", "GpsIso6709": "+27.1281+100.2508+000.000/", "CreateDate": "2024-02-02T08:09:57+00:00", "Model": "iPhone X", "DurationMs": "500" } ``` ### Parsing Files in Directory `rexiftool` also supports batch parsing of all files in a folder (non-recursive). `cargo run --example rexiftool testdata/`: ```text File: "testdata/embedded-in-heic.mov" ------------------------------------------------ Make => Apple Model => iPhone 15 Pro Software => 17.1 CreateDate => 2023-11-02T12:01:02+00:00 DurationMs => 2795 ImageWidth => 1920 ImageHeight => 1440 GpsIso6709 => +22.5797+113.9380+028.396/ File: "testdata/compatible-brands-fail.heic" ------------------------------------------------ Unrecognized file format, consider filing a bug @ https://github.com/mindeng/nom-exif. File: "testdata/webm_480.webm" ------------------------------------------------ CreateDate => 2009-09-09T09:09:09+00:00 DurationMs => 30543 ImageWidth => 480 ImageHeight => 270 File: "testdata/mka.mka" ------------------------------------------------ DurationMs => 3422 ImageWidth => 0 ImageHeight => 0 File: "testdata/exif-one-entry.heic" ------------------------------------------------ Orientation => 1 File: "testdata/no-exif.jpg" ------------------------------------------------ Error: parse failed: Exif not found File: "testdata/exif.jpg" ------------------------------------------------ ImageWidth => 3072 Model => vivo X90 Pro+ ImageHeight => 4096 ModifyDate => 2023-07-09T20:36:33+08:00 YCbCrPositioning => 1 ExifOffset => 201 MakerNote => Undefined[0x30] RecommendedExposureIndex => 454 SensitivityType => 2 ISOSpeedRatings => 454 ExposureProgram => 2 FNumber => 175/100 (1.7500) ExposureTime => 9997/1000000 (0.0100) SensingMethod => 2 SubSecTimeDigitized => 616 OffsetTimeOriginal => +08:00 SubSecTimeOriginal => 616 OffsetTime => +08:00 SubSecTime => 616 FocalLength => 8670/1000 (8.6700) Flash => 16 LightSource => 21 MeteringMode => 1 SceneCaptureType => 0 UserComment => filter: 0; fileterIntensity: 0.0; filterMask: 0; algolist: 0; ... ``` ## Changelog [CHANGELOG.md](CHANGELOG.md) nom-exif-2.5.4/README.org000064400000000000000000000041321046102023000130240ustar 00000000000000* Nom-Exif [[https://github.com/mindeng/nom-exif/actions/workflows/rust.yml/badge.svg][nom-exif workflow]] Exif/metadata parsing library written in pure Rust with [[https://github.com/rust-bakery/nom][nom]]. ** Supported File Types - Images - JPEG - HEIF/HEIC - Videos - MOV - MP4 ** Features - Zero-copy when appropriate :: Use borrowing and slicing instead of copying whenever possible. - Minimize I/O operations :: When metadata is stored at the end of a larger file (such as a MOV file), ~Seek~ rather than ~Read~ to quickly locate the location of the metadata. - Pay as you go :: When extracting Exif data, only the information corresponding to the specified Exif tags are parsed to reduce the overhead when processing a large number of files. ** Usage *** Parse Exif from Images #+begin_src rust use nom_exif::*; use nom_exif::ExifTag::*; use std::fs::File; use std::path::Path; use std::collections::HashMap; let f = File::open(Path::new("./testdata/exif.jpg")).unwrap(); let exif = parse_jpeg_exif(f).unwrap().unwrap(); assert_eq!( exif.get_value(&ImageWidth).unwrap(), Some(IfdEntryValue::U32(3072))); assert_eq!( exif.get_values(&[CreateDate, ModifyDate, DateTimeOriginal]), [ (&CreateDate, "2023:07:09 20:36:33"), (&ModifyDate, "2023:07:09 20:36:33"), (&DateTimeOriginal, "2023:07:09 20:36:33"), ] .into_iter() .map(|x| (x.0, x.1.into())) .collect::>() ); #+end_src *** Parse metadata from Videos #+begin_src rust use nom_exif::*; use std::fs::File; use std::path::Path; let f = File::open(Path::new("./testdata/meta.mov")).unwrap(); let entries = parse_mov_metadata(reader).unwrap(); assert_eq!( entries .iter() .map(|x| format!("{x:?}")) .collect::>() .join("\n"), r#"("com.apple.quicktime.make", Text("Apple")) ("com.apple.quicktime.model", Text("iPhone X")) ("com.apple.quicktime.software", Text("12.1.2")) ("com.apple.quicktime.location.ISO6709", Text("+27.1281+100.2508+000.000/")) ("com.apple.quicktime.creationdate", Text("2019-02-12T15:27:12+08:00"))"# ); #+end_src nom-exif-2.5.4/examples/rexiftool.rs000064400000000000000000000112551046102023000155610ustar 00000000000000use std::{ error::Error, fs::{self, File}, io::{self}, path::Path, process::ExitCode, }; use clap::Parser; use nom_exif::{ExifIter, MediaParser, MediaSource, TrackInfo}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, Registry}; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Cli { file: String, #[arg(short, long)] json: bool, #[arg(long)] debug: bool, } #[cfg(feature = "json_dump")] const FEATURE_JSON_DUMP_ON: bool = true; #[cfg(not(feature = "json_dump"))] const FEATURE_JSON_DUMP_ON: bool = false; fn main() -> ExitCode { let cli = Cli::parse(); tracing_run(&cli) } #[tracing::instrument] fn tracing_run(cli: &Cli) -> ExitCode { if cli.debug { init_tracing().expect("init tracing failed"); } if let Err(err) = run(cli) { tracing::error!(?err); eprintln!("{err}"); return ExitCode::FAILURE; } ExitCode::SUCCESS } fn run(cli: &Cli) -> Result<(), Box> { if cli.json && !FEATURE_JSON_DUMP_ON { let msg = "-j/--json option requires the feature `json_dump`."; eprintln!("{msg}"); return Err(msg.into()); } let mut parser = MediaParser::new(); let path = Path::new(&cli.file); if path.is_file() { let _ = parse_file(&mut parser, path, cli.json); } else if path.is_dir() { parse_dir(path, parser, cli)?; } Ok(()) } fn parse_dir(path: &Path, mut parser: MediaParser, cli: &Cli) -> Result<(), Box> { let mut first = true; for entry in fs::read_dir(path)? { if first { first = false; } else { println!(); } match entry { Ok(entry) => { let Ok(ft) = entry.file_type() else { continue; }; if !ft.is_file() { continue; } println!("File: {:?}", entry.path().as_os_str()); println!("------------------------------------------------"); let _ = parse_file(&mut parser, entry.path(), cli.json); } Err(e) => { eprintln!("Read dir entry failed: {e}"); continue; } } } Ok(()) } fn parse_file>( parser: &mut MediaParser, path: P, json: bool, ) -> Result<(), nom_exif::Error> { let ms = MediaSource::file_path(path).inspect_err(handle_parsing_error)?; let values = if ms.has_exif() { let iter: ExifIter = parser.parse(ms).inspect_err(handle_parsing_error)?; iter.into_iter() .filter_map(|mut x| { let res = x.take_result(); match res { Ok(v) => Some(( x.tag() .map(|x| x.to_string()) .unwrap_or_else(|| format!("Unknown(0x{:04x})", x.tag_code())), v, )), Err(e) => { tracing::warn!(?e); None } } }) .collect::>() } else { let info: TrackInfo = parser.parse(ms)?; info.into_iter() .map(|x| (x.0.to_string(), x.1)) .collect::>() }; if json { #[cfg(feature = "json_dump")] use std::collections::HashMap; #[cfg(feature = "json_dump")] match serde_json::to_string_pretty( &values .into_iter() .map(|x| (x.0.to_string(), x.1)) .collect::>(), ) { Ok(s) => { println!("{}", s); } Err(e) => eprintln!("Error: {e}"), } } else { values.iter().for_each(|x| { println!("{:<32}=> {}", x.0, x.1); }); }; Ok(()) } fn handle_parsing_error(e: &nom_exif::Error) { match e { nom_exif::Error::UnrecognizedFileFormat => { eprintln!("Unrecognized file format, consider filing a bug @ https://github.com/mindeng/nom-exif."); } nom_exif::Error::ParseFailed(_) | nom_exif::Error::IOError(_) => { eprintln!("Error: {e}"); } } } fn init_tracing() -> io::Result<()> { let stdout_log = tracing_subscriber::fmt::layer().pretty(); let subscriber = Registry::default().with(stdout_log); let file = File::create("debug.log")?; let debug_log = tracing_subscriber::fmt::layer() .with_ansi(false) .with_writer(file); let subscriber = subscriber.with(debug_log); subscriber.init(); Ok(()) } nom-exif-2.5.4/src/bbox/idat.rs000064400000000000000000000015161046102023000143710ustar 00000000000000use std::ops::Range; use nom::{bytes::streaming, IResult}; use crate::bbox::BoxHeader; #[derive(Debug, Clone, PartialEq, Eq)] pub struct IdatBox<'a> { header: BoxHeader, data: &'a [u8], } #[allow(unused)] impl<'a> IdatBox<'a> { pub fn parse(input: &'a [u8]) -> IResult<&'a [u8], IdatBox<'a>> { let (remain, header) = BoxHeader::parse(input)?; let box_size = usize::try_from(header.box_size).expect("box size must fit into a `usize`."); let (remain, data) = streaming::take(box_size - header.header_size)(remain)?; Ok((remain, IdatBox { header, data })) } pub fn get_data(&self, range: Range) -> crate::Result<&[u8]> { if range.len() > self.data.len() { Err("idat data is too small".into()) } else { Ok(&self.data[range]) } } } nom-exif-2.5.4/src/bbox/iinf.rs000064400000000000000000000071601046102023000143760ustar 00000000000000use std::collections::HashMap; use nom::{ bytes::streaming, combinator::{cond, fail, map_res}, error::context, multi::many_m_n, number::streaming::{be_u16, be_u32}, IResult, }; use crate::{bbox::FullBoxHeader, utils::parse_cstr}; use super::{ParseBody, ParseBox}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct IinfBox { pub(crate) header: FullBoxHeader, pub(crate) entries: HashMap, } impl ParseBody for IinfBox { fn parse_body(remain: &[u8], header: FullBoxHeader) -> IResult<&[u8], IinfBox> { let version = header.version; let (remain, item_count) = if version > 0 { be_u32(remain)? } else { map_res(be_u16, |x| Ok::(x as u32))(remain)? }; let (remain, entries) = many_m_n(item_count as usize, item_count as usize, InfeBox::parse_box)(remain)?; let entries = entries .into_iter() .map(|e| (e.key().to_owned(), e)) .collect::>(); Ok((remain, IinfBox { header, entries })) } } impl IinfBox { pub fn get_infe(&self, item_type: &'static str) -> Option<&InfeBox> { self.entries.get(item_type) } } /// Info entry box #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct InfeBox { pub header: FullBoxHeader, pub id: u32, pub protection_index: u16, pub item_type: Option, // version >= 2 pub item_name: String, content_type: Option, content_encoding: Option, uri_type: Option, } impl ParseBody for InfeBox { #[tracing::instrument(skip_all)] fn parse_body<'a>(remain: &'a [u8], header: FullBoxHeader) -> IResult<&'a [u8], InfeBox> { let version = header.version; let (remain, id) = if version > 2 { be_u32(remain)? } else { map_res(be_u16, |x| Ok::(x as u32))(remain)? }; let (remain, protection_index) = be_u16(remain)?; let (remain, item_type) = cond( version >= 2, map_res(streaming::take(4_usize), |res: &'a [u8]| { String::from_utf8(res.to_vec()) }), )(remain)?; // tracing::debug!(?header.box_type, ?item_type, ?version, "Got"); let (remain, item_name) = parse_cstr(remain).map_err(|e| { if e.is_incomplete() { context("no enough bytes for infe item name", fail::<_, (), _>)(remain).unwrap_err() } else { e } })?; let (remain, content_type, content_encoding) = if version <= 1 || (version >= 2 && item_type.as_ref().unwrap() == "mime") { let (remain, content_type) = parse_cstr(remain)?; let (remain, content_encoding) = cond(!remain.is_empty(), parse_cstr)(remain)?; (remain, Some(content_type), content_encoding) } else { (remain, None, None) }; let (remain, uri_type) = if version >= 2 && item_type.as_ref().unwrap() == "uri" { let (remain, uri_type) = parse_cstr(remain)?; (remain, Some(uri_type)) } else { (remain, None) }; Ok(( remain, InfeBox { header, id, protection_index, item_type, item_name, content_type, content_encoding, uri_type, }, )) } } impl InfeBox { fn key(&self) -> &String { self.item_type.as_ref().unwrap_or(&self.item_name) } } nom-exif-2.5.4/src/bbox/iloc.rs000064400000000000000000000123741046102023000144020ustar 00000000000000use std::collections::HashMap; use nom::{ combinator::{cond, fail, map_res}, error::context, multi::many_m_n, number::streaming::{be_u16, be_u32, be_u64, be_u8}, IResult, }; use crate::bbox::FullBoxHeader; use super::{Error, ParseBody}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct IlocBox { header: FullBoxHeader, offset_size: u8, // 4 bits length_size: u8, // 4 bits base_offset_size: u8, // 4 bits index_size: u8, // 4 bits, version 1/2, reserved in version 0 pub(crate) items: HashMap, } const MAX_ILOC_EXTENTS_PER_ITEM: u16 = 32; impl ParseBody for IlocBox { #[tracing::instrument(skip_all)] fn parse_body(remain: &[u8], header: FullBoxHeader) -> IResult<&[u8], IlocBox> { let version = header.version; let (remain, (offset_size, length_size)) = map_res(be_u8, |res| Ok::<(u8, u8), ()>((res >> 4, res & 0xF)))(remain)?; let (remain, (base_offset_size, index_size)) = map_res(be_u8, |res| Ok::<(u8, u8), ()>((res >> 4, res & 0xF)))(remain)?; let (remain, item_count) = if version < 2 { map_res(be_u16, |x| Ok::(x as u32))(remain)? } else { be_u32(remain)? }; let (remain, items) = many_m_n(item_count as usize, item_count as usize, |remain| { let (remain, item_id) = if version < 2 { map_res(be_u16, |x| Ok::(x as u32))(remain)? } else { be_u32(remain)? }; let (remain, construction_method) = cond( version >= 1, map_res(be_u16, |res| Ok::((res & 0xF) as u8)), )(remain)?; let (remain, data_ref_index) = be_u16(remain)?; let (remain, base_offset) = parse_base_offset(base_offset_size, remain, "base_offset_size is not 4 or 8")?; let (remain, extent_count) = be_u16(remain)?; if extent_count > MAX_ILOC_EXTENTS_PER_ITEM { tracing::debug!(?extent_count, "extent_count"); context("extent_count > 32", fail::<_, (), _>)(remain)?; } let (remain, extents) = many_m_n(extent_count as usize, extent_count as usize, |remain| { let (remain, index) = parse_base_offset(index_size, remain, "index_size is not 4 or 8")?; let (remain, offset) = parse_base_offset(offset_size, remain, "offset_size is not 4 or 8")?; let (remain, length) = parse_base_offset(length_size, remain, "length_size is not 4 or 8")?; Ok(( remain, ItemLocationExtent { index, offset, length, }, )) })(remain)?; Ok(( remain, ItemLocation { extents, id: item_id, construction_method, base_offset, data_ref_index, }, )) })(remain)?; Ok(( remain, IlocBox { header, offset_size, length_size, base_offset_size, index_size, items: items.into_iter().map(|x| (x.id, x)).collect(), }, )) } } impl IlocBox { pub fn item_offset_len(&self, id: u32) -> Option<(u8, u64, u64)> { self.items .get(&id) .map(|item| (item, item.extents.first())) .and_then(|(item, extent)| { extent.map(|extent| { ( item.construction_method.unwrap_or(0), item.base_offset + extent.offset, extent.length, ) }) }) } } #[derive(Debug, Clone, PartialEq, Eq)] struct ItemLocationExtent { index: u64, offset: u64, length: u64, } fn parse_base_offset<'a>(size: u8, remain: &'a [u8], msg: &'static str) -> IResult<&'a [u8], u64> { Ok(if size == 4 { map_res(be_u32, |x| Ok::(x as u64))(remain)? } else if size == 8 { be_u64(remain)? } else if size == 0 { (remain, 0) } else { context(msg, fail)(remain)? }) } #[derive(Debug, Clone, PartialEq, Eq)] pub struct ItemLocation { id: u32, /// 0: file offset, 1: idat offset, 2: item offset (currently not supported) construction_method: Option, data_ref_index: u16, base_offset: u64, extents: Vec, } #[allow(clippy::enum_variant_names)] enum ConstructionMethod { FileOffset = 0, IdatOffset = 1, ItemOffset = 2, } impl TryFrom for ConstructionMethod { type Error = Error; fn try_from(value: u8) -> std::result::Result { match value { 0 => Ok(Self::FileOffset), 1 => Ok(Self::IdatOffset), 2 => Ok(Self::ItemOffset), other => Err(Error::UnsupportedConstructionMethod(other)), } } } nom-exif-2.5.4/src/bbox/ilst.rs000064400000000000000000000174071046102023000144310ustar 00000000000000use nom::bytes::complete::{tag, take}; use nom::combinator::{fail, map_res}; use nom::error::context; use nom::multi::many0; use nom::number::complete::{ be_f32, be_f64, be_i16, be_i24, be_i32, be_i64, be_u16, be_u24, be_u32, be_u64, u8, }; use nom::sequence::tuple; use crate::EntryValue; use super::BoxHeader; /// Represents an [item list atom][1]. /// /// ilst is not a fullbox, it doesn't have version & flags. /// /// atom-path: moov/meta/ilst /// /// [1]: https://developer.apple.com/documentation/quicktime-file-format/metadata_item_list_atom #[derive(Debug, Clone, PartialEq)] pub struct IlstBox { header: BoxHeader, pub items: Vec, } impl IlstBox { pub fn parse_box(input: &[u8]) -> nom::IResult<&[u8], IlstBox> { let (remain, header) = BoxHeader::parse(input)?; let (remain, items) = many0(IlstItem::parse)(remain)?; Ok((remain, IlstBox { header, items })) } } #[derive(Debug, Clone, PartialEq)] pub struct IlstItem { size: u32, index: u32, // 1-based index (start from 1) data_len: u32, // including self size /// Type indicator, see [type /// indicator](https://developer.apple.com/documentation/quicktime-file-format/type_indicator) type_set: u8, type_code: u32, // 24-bits local: u32, pub value: EntryValue, // len: data_len - 16 } impl IlstItem { fn parse<'a>(input: &'a [u8]) -> nom::IResult<&'a [u8], IlstItem> { let (remain, (size, index, data_len, _, type_set, type_code, local)) = tuple((be_u32, be_u32, be_u32, tag("data"), u8, be_u24, be_u32))(input)?; if size < 24 || data_len < 16 { context("invalid ilst item", fail::<_, (), _>)(remain)?; } // assert_eq!(size - 24, data_len - 16); if size - 24 != data_len - 16 { context("invalid ilst item", fail::<_, (), _>)(remain)?; } let (remain, value) = map_res(take(data_len - 16), |bs: &'a [u8]| { parse_value(type_code, bs) })(remain)?; Ok(( remain, IlstItem { size, index, data_len, type_set, type_code, local, value, }, )) } } /// Parse ilst item data to value, see [Well-known /// types](https://developer.apple.com/documentation/quicktime-file-format/well-known_types) #[tracing::instrument(skip(data))] fn parse_value(type_code: u32, data: &[u8]) -> crate::Result { use EntryValue::*; let v = match type_code { 1 => { let s = String::from_utf8(data.to_vec())?; Text(s) } 21 => match data.len() { 1 => data[0].into(), 2 => be_i16(data)?.1.into(), 3 => be_i24(data)?.1.into(), 4 => be_i32(data)?.1.into(), 8 => be_i64(data)?.1.into(), data_len => { let data_type = "BE Signed Integer"; tracing::warn!(data_type, data_len, "Invalid ilst item data."); let msg = format!( "Invalid ilst item data; \ data type is {data_type} while data len is : {data_len}", ); return Err(msg.into()); } }, 22 => match data.len() { 1 => data[0].into(), 2 => be_u16(data)?.1.into(), 3 => be_u24(data)?.1.into(), 4 => be_u32(data)?.1.into(), 8 => be_u64(data)?.1.into(), data_len => { let data_type = "BE Unsigned Integer"; tracing::warn!(data_type, data_len, "Invalid ilst item data."); let msg = format!( "Invalid ilst item data; \ data type is {data_type} while data len is : {data_len}", ); return Err(msg.into()); } }, 23 => be_f32(data)?.1.into(), 24 => be_f64(data)?.1.into(), data_type => { let msg = "Unsupported ilst item data type"; tracing::warn!(data_type, "{}.", msg); return Err(format!("{}: {data_type}", msg).into()); } }; Ok(v) } #[cfg(test)] mod tests { use crate::{bbox::travel_while, testkit::read_sample}; use super::*; use test_case::test_case; #[test_case("meta.mov")] fn ilst_box(path: &str) { let _ = tracing_subscriber::fmt().with_test_writer().try_init(); let buf = read_sample(path).unwrap(); let (_, bbox) = travel_while(&buf, |b| b.box_type() != "moov").unwrap(); let bbox = bbox.unwrap(); let (_, bbox) = travel_while(bbox.body_data(), |b| b.box_type() != "meta").unwrap(); let bbox = bbox.unwrap(); let (_, bbox) = travel_while(bbox.body_data(), |b| b.box_type() != "ilst").unwrap(); let bbox = bbox.unwrap(); let (rem, ilst) = IlstBox::parse_box(bbox.data).unwrap(); tracing::info!(?ilst, "ilst"); assert_eq!(rem, b""); assert_eq!( ilst.items .iter() .map(|x| format!("{x:?}")) .collect::>(), [ "IlstItem { size: 29, index: 1, data_len: 21, type_set: 0, type_code: 1, local: 0, value: Text(\"Apple\") }", "IlstItem { size: 32, index: 2, data_len: 24, type_set: 0, type_code: 1, local: 0, value: Text(\"iPhone X\") }", "IlstItem { size: 30, index: 3, data_len: 22, type_set: 0, type_code: 1, local: 0, value: Text(\"12.1.2\") }", "IlstItem { size: 50, index: 4, data_len: 42, type_set: 0, type_code: 1, local: 0, value: Text(\"+27.1281+100.2508+000.000/\") }", "IlstItem { size: 49, index: 5, data_len: 41, type_set: 0, type_code: 1, local: 0, value: Text(\"2019-02-12T15:27:12+08:00\") }" ], ); } #[test_case("embedded-in-heic.mov")] fn heic_mov_ilst(path: &str) { let _ = tracing_subscriber::fmt().with_test_writer().try_init(); let buf = read_sample(path).unwrap(); let (_, moov) = travel_while(&buf, |b| b.box_type() != "moov").unwrap(); let moov = moov.unwrap(); let (_, meta) = travel_while(moov.body_data(), |b| b.box_type() != "meta").unwrap(); let meta = meta.unwrap(); let (_, ilst) = travel_while(meta.body_data(), |b| b.box_type() != "ilst").unwrap(); let ilst = ilst.unwrap(); let (rem, ilst) = IlstBox::parse_box(ilst.data).unwrap(); assert_eq!(rem.len(), 0); let mut s = ilst .items .iter() .map(|x| format!("{x:?}")) .collect::>() .join("\n"); s.insert(0, '\n'); assert_eq!( s, " IlstItem { size: 33, index: 1, data_len: 25, type_set: 0, type_code: 1, local: 0, value: Text(\"14.235563\") } IlstItem { size: 25, index: 2, data_len: 17, type_set: 0, type_code: 22, local: 0, value: U8(1) } IlstItem { size: 60, index: 3, data_len: 52, type_set: 0, type_code: 1, local: 0, value: Text(\"DA1A7EE8-0925-4C9F-9266-DDA3F0BB80F0\") } IlstItem { size: 28, index: 4, data_len: 20, type_set: 0, type_code: 23, local: 0, value: F32(0.93884003) } IlstItem { size: 32, index: 5, data_len: 24, type_set: 0, type_code: 21, local: 0, value: I64(4) } IlstItem { size: 50, index: 6, data_len: 42, type_set: 0, type_code: 1, local: 0, value: Text(\"+22.5797+113.9380+028.396/\") } IlstItem { size: 29, index: 7, data_len: 21, type_set: 0, type_code: 1, local: 0, value: Text(\"Apple\") } IlstItem { size: 37, index: 8, data_len: 29, type_set: 0, type_code: 1, local: 0, value: Text(\"iPhone 15 Pro\") } IlstItem { size: 28, index: 9, data_len: 20, type_set: 0, type_code: 1, local: 0, value: Text(\"17.1\") } IlstItem { size: 48, index: 10, data_len: 40, type_set: 0, type_code: 1, local: 0, value: Text(\"2023-11-02T19:58:34+0800\") }" ); } } nom-exif-2.5.4/src/bbox/keys.rs000064400000000000000000000134551046102023000144300ustar 00000000000000use nom::bytes::complete::take; use nom::combinator::{flat_map, map_res}; use nom::multi::many_m_n; use nom::number::complete::be_u32; use crate::bbox::{FullBoxHeader, ParseBody}; /// Represents a [keys atom][1]. /// /// `keys` is a fullbox which contains version & flags. /// /// atom-path: moov/meta/keys /// /// [1]: https://developer.apple.com/documentation/quicktime-file-format/metadata_item_keys_atom #[derive(Debug, Clone, PartialEq, Eq)] pub struct KeysBox { header: FullBoxHeader, entry_count: u32, pub entries: Vec, } impl ParseBody for KeysBox { fn parse_body(body: &[u8], header: FullBoxHeader) -> nom::IResult<&[u8], KeysBox> { let (remain, entry_count) = be_u32(body)?; let (remain, entries) = many_m_n(entry_count as usize, entry_count as usize, KeyEntry::parse)(remain)?; Ok(( remain, KeysBox { header, entry_count, entries, }, )) } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct KeyEntry { size: u32, pub namespace: String, // 4 bytes pub key: String, // len: size - 8 } impl KeyEntry { fn parse<'a>(input: &'a [u8]) -> nom::IResult<&'a [u8], KeyEntry> { let (remain, s) = map_res( flat_map( map_res(be_u32, |len| { len.checked_sub(4).ok_or("invalid KeyEntry header") }), take, ), |bs: &'a [u8]| String::from_utf8(bs.to_vec()), )(input)?; Ok(( remain, KeyEntry { size: (s.len() + 4) as u32, namespace: s.chars().take(4).collect(), key: s.chars().skip(4).collect(), }, )) } } #[cfg(test)] mod tests { use crate::{ bbox::{travel_while, ParseBox}, testkit::read_sample, }; use super::*; use test_case::test_case; #[test_case("meta.mov", 4133, 0x01b9, 0xc9)] fn keys_box(path: &str, moov_size: u64, meta_size: u64, keys_size: u64) { let _ = tracing_subscriber::fmt().with_test_writer().try_init(); let buf = read_sample(path).unwrap(); let (_, moov) = travel_while(&buf, |b| b.box_type() != "moov").unwrap(); let moov = moov.unwrap(); let (_, meta) = travel_while(moov.body_data(), |b| b.box_type() != "meta").unwrap(); let meta = meta.unwrap(); let (_, keys) = travel_while(meta.body_data(), |b| b.box_type() != "keys").unwrap(); let keys = keys.unwrap(); assert_eq!(moov.box_size(), moov_size); assert_eq!(meta.box_size(), meta_size); assert_eq!(keys.box_size(), keys_size); let (rem, keys) = KeysBox::parse_box(keys.data).unwrap(); assert!(rem.is_empty()); assert_eq!( keys.entries, vec![ KeyEntry { size: 32, namespace: "mdta".to_owned(), key: "com.apple.quicktime.make".to_owned() }, KeyEntry { size: 33, namespace: "mdta".to_owned(), key: "com.apple.quicktime.model".to_owned() }, KeyEntry { size: 36, namespace: "mdta".to_owned(), key: "com.apple.quicktime.software".to_owned() }, KeyEntry { size: 44, namespace: "mdta".to_owned(), key: "com.apple.quicktime.location.ISO6709".to_owned() }, KeyEntry { size: 40, namespace: "mdta".to_owned(), key: "com.apple.quicktime.creationdate".to_owned() } ] ); } #[test_case("embedded-in-heic.mov", 0x1790, 0x0372, 0x1ce)] fn heic_mov_keys(path: &str, moov_size: u64, meta_size: u64, keys_size: u64) { let _ = tracing_subscriber::fmt().with_test_writer().try_init(); let buf = read_sample(path).unwrap(); let (_, moov) = travel_while(&buf, |b| b.box_type() != "moov").unwrap(); let moov = moov.unwrap(); let (_, meta) = travel_while(moov.body_data(), |b| b.box_type() != "meta").unwrap(); let meta = meta.unwrap(); let (_, keys) = travel_while(meta.body_data(), |b| b.box_type() != "keys").unwrap(); let keys = keys.unwrap(); assert_eq!(moov.box_size(), moov_size); assert_eq!(meta.box_size(), meta_size); assert_eq!(keys.box_size(), keys_size); let (rem, keys) = KeysBox::parse_box(keys.data).unwrap(); assert!(rem.is_empty()); let mut s = keys .entries .iter() .map(|x| format!("{x:?}")) .collect::>() .join("\n"); s.insert(0, '\n'); assert_eq!( s, r#" KeyEntry { size: 56, namespace: "mdta", key: "com.apple.quicktime.location.accuracy.horizontal" } KeyEntry { size: 43, namespace: "mdta", key: "com.apple.quicktime.live-photo.auto" } KeyEntry { size: 46, namespace: "mdta", key: "com.apple.quicktime.content.identifier" } KeyEntry { size: 53, namespace: "mdta", key: "com.apple.quicktime.live-photo.vitality-score" } KeyEntry { size: 63, namespace: "mdta", key: "com.apple.quicktime.live-photo.vitality-scoring-version" } KeyEntry { size: 44, namespace: "mdta", key: "com.apple.quicktime.location.ISO6709" } KeyEntry { size: 32, namespace: "mdta", key: "com.apple.quicktime.make" } KeyEntry { size: 33, namespace: "mdta", key: "com.apple.quicktime.model" } KeyEntry { size: 36, namespace: "mdta", key: "com.apple.quicktime.software" } KeyEntry { size: 40, namespace: "mdta", key: "com.apple.quicktime.creationdate" }"#, ); } } nom-exif-2.5.4/src/bbox/meta.rs000064400000000000000000000134031046102023000143740ustar 00000000000000use std::{collections::HashMap, fmt::Debug, ops::Range}; use nom::{combinator::fail, multi::many0, IResult, Needed}; use crate::bbox::FullBoxHeader; use super::{iinf::IinfBox, iloc::IlocBox, BoxHolder, ParseBody, ParseBox}; /// Representing the `meta` box in a HEIF/HEIC file. #[derive(Clone, PartialEq, Eq)] pub struct MetaBox { header: FullBoxHeader, iinf: Option, iloc: Option, // idat: Option>, } impl Debug for MetaBox { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("MetaBox") .field("header", &self.header) .field( "iinf entries num", &self.iinf.as_ref().map(|x| x.entries.len()), ) .field("iloc items num", &self.iloc.as_ref().map(|x| x.items.len())) .finish() } } impl ParseBody for MetaBox { fn parse_body<'a>(remain: &'a [u8], header: FullBoxHeader) -> IResult<&'a [u8], MetaBox> { let (remain, boxes) = many0(|remain: &'a [u8]| { if remain.is_empty() { // stop many0 parsing to prevent Incomplete error fail::<_, (), _>(remain)?; } let (remain, bbox) = BoxHolder::parse(remain)?; Ok((remain, bbox)) })(remain)?; let boxes = boxes .into_iter() .map(|b| (b.header.box_type.to_owned(), b)) .collect::>(); // parse iinf box let iinf = boxes .get("iinf") .map(|iinf| IinfBox::parse_box(iinf.data)) .transpose()? .map(|x| x.1); // parse iloc box let iloc = boxes .get("iloc") .map(|iloc| IlocBox::parse_box(iloc.data)) .transpose()? .map(|x| x.1); // parse idat box // let idat = boxes // .get("idat") // .and_then(|idat| Some(IdatBox::parse(idat.data))) // .transpose()? // .map(|x| x.1); Ok(( remain, MetaBox { header, iinf, iloc, // idat, }, )) } } impl MetaBox { #[tracing::instrument(skip_all)] pub fn exif_data<'a>(&self, input: &'a [u8]) -> IResult<&'a [u8], Option<&'a [u8]>> { self.iinf .as_ref() .and_then(|iinf| iinf.get_infe("Exif")) .and_then(|exif_infe| { self.iloc .as_ref() .and_then(|iloc| iloc.item_offset_len(exif_infe.id)) }) .map(|(construction_method, offset, length)| { let start = offset as usize; let end = (offset + length) as usize; if construction_method == 0 { // file offset if end > input.len() { Err(nom::Err::Incomplete(Needed::new(end - input.len()))) } else { Ok((&input[end..], Some(&input[start..end]))) // Safe-slice } } else if construction_method == 1 { // idat offset tracing::debug!("idat offset construction method is not supported yet"); fail(input) } else { tracing::debug!("item offset construction method is not supported yet"); fail(input) } }) .unwrap_or(Ok((input, None))) } #[tracing::instrument(skip_all)] pub fn exif_data_offset(&self) -> Option> { self.iinf .as_ref() .and_then(|iinf| iinf.get_infe("Exif")) .and_then(|exif_infe| { self.iloc .as_ref() .and_then(|iloc| iloc.item_offset_len(exif_infe.id)) }) .and_then(|(construction_method, offset, length)| { let start = offset as usize; let end = (offset + length) as usize; if construction_method == 0 { // file offset Some(start..end) } else if construction_method == 1 { // idat offset tracing::debug!("idat offset construction method is not supported yet"); None } else { tracing::debug!("item offset construction method is not supported yet"); None } }) } } #[derive(Debug, Clone, PartialEq, Eq)] struct ItemLocationExtent { index: u64, offset: u64, length: u64, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct ItemLocation { id: u32, /// 0: file offset, 1: idat offset, 2: item offset (currently not supported) construction_method: Option, data_ref_index: u16, base_offset: u64, extents: Vec, } #[cfg(test)] mod tests { use crate::{bbox::travel_while, testkit::read_sample}; use super::*; use test_case::test_case; #[test_case("exif.heic", 2618)] fn meta(path: &str, meta_size: usize) { let _ = tracing_subscriber::fmt().with_test_writer().try_init(); let buf = read_sample(path).unwrap(); let (_, bbox) = travel_while(&buf, |bbox| { tracing::info!(bbox.header.box_type, "Got"); bbox.box_type() != "meta" }) .unwrap(); let bbox = bbox.unwrap(); assert_eq!(bbox.data.len() as u64, bbox.box_size()); let (remain, meta) = MetaBox::parse_box(bbox.data).unwrap(); assert_eq!(remain, b""); assert_eq!(meta.header.box_type, "meta"); assert_eq!(meta.exif_data(&buf).unwrap().1.unwrap().len(), meta_size); } } nom-exif-2.5.4/src/bbox/mvhd.rs000064400000000000000000000075611046102023000144140ustar 00000000000000use chrono::{DateTime, Duration, FixedOffset, Local, NaiveDate, NaiveDateTime, TimeZone, Utc}; use nom::{bytes::complete::take, number::complete::be_u32, sequence::tuple}; use super::{FullBoxHeader, ParseBody}; /// Represents a [movie header atom][1]. /// /// mvhd is a fullbox which contains version & flags. /// /// atom-path: moov/mvhd /// /// [1]: https://developer.apple.com/documentation/quicktime-file-format/movie_header_atom #[derive(Debug, Clone, PartialEq, Eq)] pub struct MvhdBox { header: FullBoxHeader, /// seconds since midnight, January 1, 1904 creation_time: u32, /// seconds since midnight, January 1, 1904 modification_time: u32, /// The number of time units that pass per second in its time coordinate /// system. time_scale: u32, /// Indicates the duration of the movie in time scale units. /// /// # convert to seconds /// /// seconds = duration / time_scale duration: u32, // omit 76 bytes... next_track_id: u32, } impl MvhdBox { pub fn duration_ms(&self) -> u64 { ((self.duration as f64) / (self.time_scale as f64) * 1000_f64) as u64 } fn creation_time_naive(&self) -> NaiveDateTime { NaiveDate::from_ymd_opt(1904, 1, 1) .unwrap() .and_hms_opt(0, 0, 0) .unwrap() + Duration::seconds(self.creation_time as i64) } pub fn creation_time(&self) -> DateTime { self.creation_time_utc().fixed_offset() } #[allow(dead_code)] pub fn creation_time_local(&self) -> DateTime { Local.from_utc_datetime(&self.creation_time_naive()) } pub fn creation_time_utc(&self) -> DateTime { self.creation_time_naive().and_utc() } } impl ParseBody for MvhdBox { fn parse_body(body: &[u8], header: FullBoxHeader) -> nom::IResult<&[u8], MvhdBox> { let (remain, (creation_time, modification_time, time_scale, duration, _, next_track_id)) = tuple((be_u32, be_u32, be_u32, be_u32, take(76usize), be_u32))(body)?; Ok(( remain, MvhdBox { header, creation_time, modification_time, time_scale, duration, next_track_id, }, )) } } #[cfg(test)] mod tests { use crate::{ bbox::{travel_while, ParseBox}, testkit::read_sample, }; use super::*; use chrono::FixedOffset; use test_case::test_case; #[test_case( "meta.mov", "2024-02-02T08:09:57.000000Z", "2024-02-02T16:09:57+08:00", 500 )] #[test_case( "meta.mp4", "2024-02-03T07:05:38.000000Z", "2024-02-03T15:05:38+08:00", 1063 )] fn mvhd_box(path: &str, time_utc: &str, time_east8: &str, milliseconds: u64) { let _ = tracing_subscriber::fmt().with_test_writer().try_init(); let buf = read_sample(path).unwrap(); let (_, bbox) = travel_while(&buf, |b| b.box_type() != "moov").unwrap(); let bbox = bbox.unwrap(); let (_, bbox) = travel_while(bbox.body_data(), |b| b.box_type() != "mvhd").unwrap(); let bbox = bbox.unwrap(); let (_, mvhd) = MvhdBox::parse_box(bbox.data).unwrap(); assert_eq!(mvhd.duration_ms(), milliseconds); // time is represented in seconds since midnight, January 1, 1904, // preferably using coordinated universal time (UTC). let created = mvhd.creation_time_utc(); assert_eq!(created, mvhd.creation_time()); assert_eq!( created.to_rfc3339_opts(chrono::SecondsFormat::Micros, true), time_utc ); assert_eq!( created .with_timezone(&FixedOffset::east_opt(8 * 3600).unwrap()) .to_rfc3339_opts(chrono::SecondsFormat::Secs, true), time_east8 ); } } nom-exif-2.5.4/src/bbox/tkhd.rs000064400000000000000000000107671046102023000144120ustar 00000000000000use nom::{ bytes::complete::take, number::complete::{be_u16, be_u32, be_u64}, sequence::tuple, }; use super::{find_box, travel_while, BoxHolder, FullBoxHeader, ParseBody, ParseBox}; /// Represents a [movie header atom][1]. /// /// tkhd is a fullbox which contains version & flags. /// /// atom-path: moov/trak/tkhd /// /// [1]: https://developer.apple.com/documentation/quicktime-file-format/track_header_atom #[derive(Debug, Clone, PartialEq, Eq)] pub struct TkhdBox { header: FullBoxHeader, /// seconds since midnight, January 1, 1904 creation_time: u32, /// seconds since midnight, January 1, 1904 modification_time: u32, track_id: u32, // reserved: u32, duration: u32, // reserved2: u64, layer: u16, alt_group: u16, volume: u16, // reserved3: u16, // matrix: [u8; 36], pub width: u32, pub height: u32, } impl ParseBody for TkhdBox { fn parse_body(body: &[u8], header: FullBoxHeader) -> nom::IResult<&[u8], TkhdBox> { let ( remain, ( creation_time, modification_time, track_id, _, duration, _, layer, alt_group, volume, _, _, width, _, height, _, ), ) = tuple(( be_u32, be_u32, be_u32, be_u32, be_u32, be_u64, be_u16, be_u16, be_u16, be_u16, take(36usize), be_u16, be_u16, be_u16, be_u16, ))(body)?; Ok(( remain, TkhdBox { header, creation_time, modification_time, track_id, duration, layer, alt_group, volume, width: width as u32, height: height as u32, }, )) } } /// Try to find a video track's tkhd in moov body. atom-path: "moov/trak/tkhd". pub fn parse_video_tkhd_in_moov(input: &[u8]) -> crate::Result> { let Some(bbox) = find_video_track(input)? else { return Ok(None); }; let (_, Some(bbox)) = find_box(bbox.body_data(), "tkhd")? else { return Ok(None); }; let (_, tkhd) = TkhdBox::parse_box(bbox.data).map_err(|_| "parse tkhd failed")?; Ok(Some(tkhd)) } fn find_video_track(input: &[u8]) -> crate::Result> { let (_, bbox) = travel_while(input, |b| { // find video track if b.box_type() != "trak" { true } else { // got a 'trak', to check if it's a 'vide' trak let found = find_box(b.body_data(), "mdia/hdlr"); let Ok(bbox) = found else { return true; }; let Some(hdlr) = bbox.1 else { return true; }; // component subtype if hdlr.body_data().len() < 12 { return true; } let subtype = &hdlr.body_data()[8..12]; // Safe-slice if subtype == b"vide" { // found it! false } else { true } } }) .map_err(|e| format!("find vide trak failed: {e:?}"))?; Ok(bbox) } #[cfg(test)] mod tests { use crate::{bbox::travel_while, testkit::read_sample}; use super::*; use test_case::test_case; #[test_case("meta.mov", 720, 1280)] #[test_case("meta.mp4", 1920, 1080)] fn tkhd_box(path: &str, width: u32, height: u32) { let _ = tracing_subscriber::fmt().with_test_writer().try_init(); let buf = read_sample(path).unwrap(); let (_, bbox) = travel_while(&buf, |b| b.box_type() != "moov").unwrap(); let bbox = bbox.unwrap(); let tkhd = parse_video_tkhd_in_moov(bbox.body_data()).unwrap().unwrap(); assert_eq!(tkhd.width, width); assert_eq!(tkhd.height, height); } #[test_case("crash_moov-trak")] fn tkhd_crash(path: &str) { let _ = tracing_subscriber::fmt().with_test_writer().try_init(); let buf = read_sample(path).unwrap(); let (_, bbox) = travel_while(&buf, |b| b.box_type() != "moov").unwrap(); let bbox = bbox.unwrap(); let _ = parse_video_tkhd_in_moov(bbox.body_data()); } } nom-exif-2.5.4/src/bbox.rs000064400000000000000000000426551046102023000134610ustar 00000000000000use std::fmt::{Debug, Display}; use nom::{ bytes::streaming, combinator::{fail, map_res}, error::context, number, AsChar, IResult, Needed, }; mod idat; mod iinf; mod iloc; mod ilst; mod keys; mod meta; mod mvhd; mod tkhd; pub use ilst::IlstBox; pub use keys::KeysBox; pub use meta::MetaBox; pub use mvhd::MvhdBox; pub use tkhd::parse_video_tkhd_in_moov; const MAX_BODY_LEN: usize = 2000 * 1024 * 1024; #[derive(Debug, PartialEq)] pub enum Error { UnsupportedConstructionMethod(u8), } impl std::error::Error for Error {} impl Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Error::UnsupportedConstructionMethod(x) => { Debug::fmt(&format!("unsupported construction method ({x})"), f) } } } } /// Representing an ISO base media file format box header. #[derive(Debug, Clone, PartialEq, Eq)] pub struct BoxHeader { pub box_size: u64, pub box_type: String, pub header_size: usize, // include size, type } impl BoxHeader { pub fn parse<'a>(input: &'a [u8]) -> IResult<&'a [u8], BoxHeader> { let (remain, size) = number::streaming::be_u32(input)?; let (remain, box_type) = map_res(streaming::take(4_usize), |res: &'a [u8]| { // String::from_utf8 will fail on "ยฉxyz" Ok::(res.iter().map(|b| b.as_char()).collect::()) // String::from_utf8(res.to_vec()).map_err(|error| { // tracing::error!(?error, ?res, "Failed to construct string"); // error // }) })(remain)?; let (remain, box_size) = if size == 1 { number::streaming::be_u64(remain)? } else if size < 8 { context("invalid box header: box_size is too small", fail)(remain)? } else { (remain, size as u64) }; let header_size = input.len() - remain.len(); assert!(header_size == 8 || header_size == 16); if box_size < header_size as u64 { return fail(remain); } Ok(( remain, BoxHeader { box_size, box_type, header_size, }, )) } pub fn body_size(&self) -> u64 { self.box_size - self.header_size as u64 } } /// Representing an ISO base media file format full box header. #[derive(Debug, Clone, PartialEq, Eq)] pub struct FullBoxHeader { pub box_size: u64, pub box_type: String, pub header_size: usize, // include size, type, version, flags version: u8, // 8 bits flags: u32, // 24 bits } impl FullBoxHeader { fn parse(input: &[u8]) -> IResult<&[u8], FullBoxHeader> { let (remain, header) = BoxHeader::parse(input)?; let (remain, version) = number::streaming::u8(remain)?; let (remain, flags) = number::streaming::be_u24(remain)?; let header_size = input.len() - remain.len(); assert!(header_size == 12 || header_size == 20); if header.box_size < header_size as u64 { return fail(remain); } Ok(( remain, FullBoxHeader { box_type: header.box_type, box_size: header.box_size, header_size, version, flags, }, )) } pub fn body_size(&self) -> u64 { self.box_size - self.header_size as u64 } } /// Representing a generic ISO base media file format box. #[derive(Clone, PartialEq, Eq)] pub struct BoxHolder<'a> { pub header: BoxHeader, // Including header pub data: &'a [u8], } impl Debug for BoxHolder<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("BoxHolder") .field("header", &self.header) .field("body_size", &self.body_data().len()) .field( "data", &(self .body_data() .iter() .take(64) .map(|x| x.as_char()) .collect::()), ) .finish() } } impl<'a> BoxHolder<'a> { #[tracing::instrument(skip_all)] pub fn parse(input: &'a [u8]) -> IResult<&'a [u8], BoxHolder<'a>> { let (_, header) = BoxHeader::parse(input)?; tracing::debug!(box_type = header.box_type, ?header, "Got"); let box_size = usize::try_from(header.box_size) .expect("header box size should always fit into a `usize`."); let (remain, data) = streaming::take(box_size)(input)?; Ok((remain, BoxHolder { header, data })) } #[allow(unused)] pub fn box_size(&self) -> u64 { self.header.box_size } pub fn box_type(&self) -> &str { &self.header.box_type } pub fn header_size(&self) -> usize { self.header.header_size } pub fn body_data(&self) -> &'a [u8] { &self.data[self.header_size()..] // Safe-slice } } type BoxResult<'a> = IResult<&'a [u8], Option>>; pub fn to_boxes(input: &[u8]) -> crate::Result>> { let mut res = Vec::new(); let mut remain = input; loop { if remain.is_empty() { break; } let (rem, bbox) = BoxHolder::parse(remain)?; res.push(bbox); // Sanity check, to avoid infinite loops caused by unexpected errors. assert!(rem.len() < remain.len()); remain = rem; } Ok(res) } /// Parses every top level box while `predicate` returns true, then returns the /// last parsed box. pub fn travel_while<'a, F>(input: &'a [u8], mut predicate: F) -> BoxResult<'a> where F: FnMut(&BoxHolder<'a>) -> bool, { let mut remain = input; loop { if remain.is_empty() { return Ok((remain, None)); } let (rem, bbox) = BoxHolder::parse(remain)?; // Sanity check, to avoid infinite loops caused by unexpected errors. assert!(rem.len() < remain.len()); remain = rem; if !predicate(&bbox) { return Ok((remain, Some(bbox))); } } } pub fn travel_header<'a, F>(input: &'a [u8], mut predicate: F) -> IResult<&'a [u8], BoxHeader> where F: FnMut(&BoxHeader, &'a [u8]) -> bool, { let mut remain = input; loop { let (rem, header) = BoxHeader::parse(remain)?; // Sanity check, to avoid infinite loops caused by unexpected errors. assert!(rem.len() < remain.len()); remain = rem; if !predicate(&header, rem) { break Ok((rem, header)); } if remain.len() < header.body_size() as usize { return Err(nom::Err::Incomplete(Needed::new( header.body_size() as usize - remain.len(), ))); } // skip box body remain = &remain[header.body_size() as usize..]; // Safe-slice } } #[allow(unused)] /// Find a box by atom `path`, which is separated by '/', e.g.: "meta/iloc". pub fn find_box<'a>(input: &'a [u8], path: &str) -> IResult<&'a [u8], Option>> { if path.is_empty() { return Ok((input, None)); } let mut bbox = None; let mut remain = input; let mut data = input; for box_type in path.split('/').filter(|x| !x.is_empty()) { assert!(!box_type.is_empty()); let (rem, b) = find_box_by_type(data, box_type)?; let Some(b) = b else { return Ok((rem, None)); }; data = b.body_data(); (remain, bbox) = (rem, Some(b)); } Ok((remain, bbox)) } fn find_box_by_type<'a>( input: &'a [u8], box_type: &str, ) -> IResult<&'a [u8], Option>> { let mut remain = input; loop { if remain.is_empty() { return Ok((remain, None)); } let (rem, bbox) = BoxHolder::parse(remain)?; // Sanity check, to avoid infinite loops caused by unexpected errors. assert!(rem.len() < remain.len()); remain = rem; if bbox.box_type() == box_type { return Ok((rem, Some(bbox))); } } } trait ParseBody { fn parse_body(body: &[u8], header: FullBoxHeader) -> IResult<&[u8], O>; } pub trait ParseBox { fn parse_box(input: &[u8]) -> IResult<&[u8], O>; } /// auto implements parse_box for each Box which implements ParseBody impl> ParseBox for T { #[tracing::instrument(skip_all)] fn parse_box(input: &[u8]) -> IResult<&[u8], O> { let (remain, header) = FullBoxHeader::parse(input)?; assert_eq!(input.len(), header.header_size + remain.len()); assert!( header.box_size >= header.header_size as u64, "box_size = {}, header_size = {}", header.box_size, header.header_size ); // limit parsing size let box_size = header.body_size() as usize; if box_size > MAX_BODY_LEN { tracing::error!(?header.box_type, ?box_size, "Box is too big"); return fail(remain); } let (remain, data) = streaming::take(box_size)(remain)?; assert_eq!(input.len(), header.header_size + data.len() + remain.len()); let (rem, bbox) = Self::parse_body(data, header)?; if !rem.is_empty() { // TODO: Body data is not exhausted, should report this error with // tracing } Ok((remain, bbox)) } } #[cfg(test)] mod tests { use crate::testkit::read_sample; use super::*; use nom::error::make_error; use test_case::test_case; #[test_case("exif.heic")] fn travel_heic(path: &str) { let _ = tracing_subscriber::fmt().with_test_writer().try_init(); let buf = read_sample(path).unwrap(); let mut boxes = Vec::new(); let (remain, bbox) = travel_while(&buf, |bbox| { tracing::info!(bbox.header.box_type, "Got"); boxes.push((bbox.header.box_type.to_owned(), bbox.to_owned())); bbox.box_type() != "mdat" }) .unwrap(); let bbox = bbox.unwrap(); assert_eq!(bbox.header.box_type, "mdat"); assert_eq!(remain, b""); let (types, _): (Vec<_>, Vec<_>) = boxes.iter().cloned().unzip(); // top level boxes assert_eq!(types, ["ftyp", "meta", "mdat"],); let (_, meta) = boxes.remove(1); assert_eq!(meta.box_type(), "meta"); let mut boxes = Vec::new(); let (remain, bbox) = travel_while( &meta.body_data()[4..], // Safe-slice in test_case |bbox| { tracing::info!(bbox.header.box_type, "Got"); boxes.push(bbox.header.box_type.to_owned()); bbox.box_type() != "iloc" }, ) .unwrap(); let bbox = bbox.unwrap(); assert_eq!(bbox.box_type(), "iloc"); assert_eq!(remain, b""); // sub-boxes in meta assert_eq!( boxes, ["hdlr", "dinf", "pitm", "iinf", "iref", "iprp", "idat", "iloc"], ); } #[test_case("meta.mov")] fn travel_mov(path: &str) { let _ = tracing_subscriber::fmt().with_test_writer().try_init(); let buf = read_sample(path).unwrap(); let mut boxes = Vec::new(); let (remain, bbox) = travel_while(&buf, |bbox| { tracing::info!(bbox.header.box_type, "Got"); boxes.push((bbox.header.box_type.to_owned(), bbox.to_owned())); bbox.box_type() != "moov" }) .unwrap(); let bbox = bbox.unwrap(); assert_eq!(bbox.header.box_type, "moov"); assert_eq!(remain, b""); let (types, _): (Vec<_>, Vec<_>) = boxes.iter().cloned().unzip(); // top level boxes assert_eq!(types, ["ftyp", "wide", "mdat", "moov"],); let (_, moov) = boxes.pop().unwrap(); assert_eq!(moov.box_type(), "moov"); let mut boxes = Vec::new(); let (remain, bbox) = travel_while(moov.body_data(), |bbox| { tracing::info!(bbox.header.box_type, "Got"); boxes.push(bbox.header.box_type.to_owned()); bbox.box_type() != "meta" }) .unwrap(); let bbox = bbox.unwrap(); assert_eq!(bbox.box_type(), "meta"); assert_eq!(remain, b""); // sub-boxes in moov assert_eq!(boxes, ["mvhd", "trak", "trak", "trak", "trak", "meta"],); let meta = bbox; let mut boxes = Vec::new(); let (remain, _) = travel_while(meta.body_data(), |bbox| { tracing::info!(bbox.header.box_type, "Got"); boxes.push(bbox.header.box_type.to_owned()); bbox.box_type() != "ilst" }) .unwrap(); assert_eq!(remain, b""); // sub-boxes in meta assert_eq!(boxes, ["hdlr", "keys", "ilst"],); } #[test_case("meta.mp4")] fn travel_mp4(path: &str) { let _ = tracing_subscriber::fmt().with_test_writer().try_init(); let buf = read_sample(path).unwrap(); let mut boxes = Vec::new(); let (remain, bbox) = travel_while(&buf, |bbox| { tracing::info!(bbox.header.box_type, "Got"); boxes.push((bbox.header.box_type.to_owned(), bbox.to_owned())); bbox.box_type() != "moov" }) .unwrap(); let bbox = bbox.unwrap(); assert_eq!(bbox.header.box_type, "moov"); assert_eq!(remain, b""); let (types, _): (Vec<_>, Vec<_>) = boxes.iter().cloned().unzip(); // top level boxes assert_eq!(types, ["ftyp", "mdat", "moov"],); let (_, moov) = boxes.pop().unwrap(); assert_eq!(moov.box_type(), "moov"); let mut boxes = Vec::new(); let (remain, bbox) = travel_while(moov.body_data(), |bbox| { tracing::info!(bbox.header.box_type, "Got"); boxes.push((bbox.header.box_type.to_owned(), bbox.to_owned())); bbox.box_type() != "udta" }) .unwrap(); let bbox = bbox.unwrap(); assert_eq!(bbox.box_type(), "udta"); assert_eq!(remain, b""); // sub-boxes in moov assert_eq!( boxes.iter().map(|x| x.0.to_owned()).collect::>(), ["mvhd", "trak", "trak", "udta"], ); let (_, trak) = boxes.iter().find(|x| x.0 == "trak").unwrap(); let meta = bbox; let mut boxes = Vec::new(); let (remain, _) = travel_while(meta.body_data(), |bbox| { tracing::info!(bbox.header.box_type, "Got"); boxes.push(bbox.header.box_type.to_owned()); bbox.box_type() != "ยฉxyz" }) .unwrap(); assert_eq!(remain, b""); // sub-boxes in udta assert_eq!(boxes, ["ยฉxyz"],); let mut boxes = Vec::new(); let (remain, bbox) = travel_while(trak.body_data(), |bbox| { tracing::info!(bbox.header.box_type, "Got"); boxes.push(bbox.header.box_type.to_owned()); bbox.box_type() != "mdia" }) .unwrap(); assert_eq!(remain, b""); // sub-boxes in trak assert_eq!(boxes, ["tkhd", "edts", "mdia"],); let mdia = bbox.unwrap(); let mut boxes = Vec::new(); let (remain, _) = travel_while(mdia.body_data(), |bbox| { tracing::info!(bbox.header.box_type, "Got"); boxes.push(bbox.header.box_type.to_owned()); bbox.box_type() != "minf" }) .unwrap(); assert_eq!(remain, b""); // sub-boxes in mdia assert_eq!(boxes, ["mdhd", "hdlr", "minf"],); } // For mp4 files, Android phones store GPS info in the `moov/udta/ยฉxyz` // atom. #[test_case("meta.mp4")] fn find_android_gps_box(path: &str) { let _ = tracing_subscriber::fmt().with_test_writer().try_init(); let buf = read_sample(path).unwrap(); let (_, bbox) = find_box(&buf, "moov/udta/ยฉxyz").unwrap(); let bbox = bbox.unwrap(); tracing::info!(?bbox.header, "bbox"); // gps info assert_eq!( "+27.2939+112.6932/", std::str::from_utf8(&bbox.body_data()[4..]).unwrap() // Safe-slice in test_case ); } #[test] fn box_header() { let _ = tracing_subscriber::fmt().with_test_writer().try_init(); let data = [ 0x00, 0x00, 0x01, 0xdd, 0x6d, 0x65, 0x74, 0x61, 0x02, 0x04, 0x04, 0x00, ]; let (remain, header) = FullBoxHeader::parse(&data).unwrap(); assert_eq!(header.box_type, "meta"); assert_eq!(header.box_size, 0x01dd); assert_eq!(header.version, 0x2); assert_eq!(header.flags, 0x40400,); assert_eq!(header.header_size, 12); assert_eq!(remain, b""); let data = [ 0x00, 0x00, 0x00, 0x01, 0x6d, 0x64, 0x61, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0xfa, 0x74, 0x01, 0x04, 0x04, 0x00, ]; let (remain, header) = FullBoxHeader::parse(&data).unwrap(); assert_eq!(header.box_type, "mdat"); assert_eq!(header.box_size, 0xefa74); assert_eq!(header.version, 0x1); assert_eq!(header.flags, 0x40400,); assert_eq!(header.header_size, 20); assert_eq!(remain, b""); let data = [0x00, 0x00, 0x01, 0xdd, 0x6d, 0x65, 0x74]; let err = BoxHeader::parse(&data).unwrap_err(); assert!(err.is_incomplete()); let data = [0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00]; let err = BoxHeader::parse(&data).unwrap_err(); assert_eq!( err, nom::Err::Error(make_error(&[] as &[u8], nom::error::ErrorKind::Fail)) ); } } nom-exif-2.5.4/src/buffer.rs000064400000000000000000000205101046102023000137620ustar 00000000000000use std::{ collections::VecDeque, fmt::Debug, sync::{atomic::AtomicUsize, Arc}, }; use crate::parser::INIT_BUF_SIZE; // Set a reasonable value to avoid causing frequent memory allocations const MAX_REUSE_BUF_SIZE: usize = 1024 * 1024; const INIT_POOLED_BUF: usize = 2; const MAX_POOLED_BUF: usize = 8; pub(crate) struct Buffers { shared: VecDeque>>, pool: VecDeque>, acquired: AtomicUsize, } impl Buffers { pub fn new() -> Self { Self::default() } #[tracing::instrument(skip_all)] pub fn release(&mut self, mut buf: Vec) { if self.pooled() >= MAX_POOLED_BUF { // buf dropped } else { // buf pooled Self::clean(&mut buf); self.pool.push_back(buf); } self.checked_sub_acquired(); tracing::debug!(?self, "buffers status"); } #[tracing::instrument(skip_all)] pub fn release_to_share(&mut self, buf: Vec) -> Arc> { let arc = Arc::new(buf); self.shared.push_back(arc.clone()); self.checked_sub_acquired(); tracing::debug!(?self, "buffers status"); arc } #[tracing::instrument(skip_all)] pub fn acquire(&mut self) -> Vec { let buf = if let Some(buf) = self.pool.pop_front() { tracing::debug!(?self, "acquired: pooled"); buf } else if let Some(buf) = self.recycle() { tracing::debug!(?self, "acquired: recycled"); buf } else { tracing::debug!(?self, "acquired: new"); new_buf() }; let prev = self .acquired .fetch_add(1, std::sync::atomic::Ordering::Relaxed); if prev == usize::MAX { panic!("too many acquired buffers"); } tracing::debug!(?self, "buffers status"); buf } fn recycle(&mut self) -> Option> { let mut remain = VecDeque::new(); let buf = loop { let Some(arc) = self.shared.pop_front() else { break None; }; match Arc::try_unwrap(arc) { Ok(mut buf) => { // recycled Self::clean(&mut buf); break Some(buf); } Err(arc) => { // still being used, put it back remain.push_back(arc); } } }; self.shared.append(&mut remain); buf } #[allow(unused)] fn shared(&self) -> usize { self.shared.len() } fn pooled(&self) -> usize { self.pool.len() } fn acquired(&self) -> usize { self.acquired.load(std::sync::atomic::Ordering::Relaxed) } fn clean(buf: &mut Vec) { buf.clear(); if buf.capacity() > MAX_REUSE_BUF_SIZE { buf.shrink_to(MAX_REUSE_BUF_SIZE); } } fn checked_sub_acquired(&mut self) { let prev = self .acquired .fetch_sub(1, std::sync::atomic::Ordering::Relaxed); if prev == 0 { tracing::error!("released wrong buf"); debug_assert!(false, "released wrong buf"); } } } impl Default for Buffers { fn default() -> Self { let mut pool = VecDeque::new(); for _ in 0..INIT_POOLED_BUF { pool.push_back(new_buf()); } Self { shared: VecDeque::new(), pool, acquired: AtomicUsize::new(0), } } } fn new_buf() -> Vec { Vec::with_capacity(INIT_BUF_SIZE) } impl Debug for Buffers { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Buffers") .field("acquired", &self.acquired()) .field("shared", &self.shared.len()) .field("pool", &self.pool.len()) .finish() } } #[cfg(test)] mod tests { use std::cmp::min; use crate::buffer::{INIT_POOLED_BUF, MAX_POOLED_BUF}; use super::Buffers; #[test] fn buffers_prior_to_take_pooled() { let mut bb = Buffers::new(); assert_eq!(bb.acquired(), 0); assert_eq!(bb.pooled(), INIT_POOLED_BUF); assert_eq!(bb.shared(), 0); const NUM: usize = MAX_POOLED_BUF + 1; let mut bufs = Vec::with_capacity(2 * NUM); for i in 1..=2 * NUM { let buf = bb.acquire(); assert_eq!(bb.acquired(), i); assert_eq!(bb.shared(), 0); assert_eq!(bb.pooled(), INIT_POOLED_BUF.saturating_sub(i)); bufs.push(buf); } assert_eq!(bb.acquired(), 2 * NUM); assert_eq!(bb.shared(), 0); assert_eq!(bb.pooled(), 0); let mut shared = Vec::with_capacity(NUM); for i in 1..=NUM { let arc = bb.release_to_share(bufs.pop().unwrap()); assert_eq!(bb.acquired(), 2 * NUM - i); assert_eq!(bb.shared(), i); assert_eq!(bb.pooled(), 0); shared.push(arc); } assert_eq!(bb.acquired(), NUM); assert_eq!(bb.shared(), NUM); assert_eq!(bb.pooled(), 0); for i in 1..=NUM { bb.release(bufs.pop().unwrap()); assert_eq!(bb.acquired(), NUM - i); assert_eq!(bb.shared(), NUM); assert_eq!(bb.pooled(), min(i, MAX_POOLED_BUF)); } assert_eq!(bb.acquired(), 0); assert_eq!(bb.shared(), NUM); assert_eq!(bb.pooled(), min(NUM, MAX_POOLED_BUF)); for i in 1..=NUM { drop(shared.pop().unwrap()); let buf = bb.acquire(); let take_pooled = i <= MAX_POOLED_BUF; assert_eq!(bb.acquired(), i); assert_eq!( bb.shared(), if take_pooled { NUM } else { NUM + MAX_POOLED_BUF - i }, "i: {i}" ); assert_eq!( bb.pooled(), min(NUM, MAX_POOLED_BUF).saturating_sub(i), "i: {i}" ); bufs.push(buf); } assert_eq!(bb.acquired(), NUM); assert_eq!(bb.shared(), NUM - 1); assert_eq!(bb.pooled(), 0); for i in 1..=NUM { bb.acquire(); assert_eq!(bb.acquired(), NUM + i); assert_eq!(bb.shared(), NUM.saturating_sub(i).saturating_sub(1)); assert_eq!(bb.pooled(), 0); } assert_eq!(bb.acquired(), 2 * NUM); assert_eq!(bb.shared(), 0); assert_eq!(bb.pooled(), 0); } #[test] fn buffers_max_pooled() { let mut bb = Buffers::new(); assert_eq!(bb.acquired(), 0); assert_eq!(bb.pooled(), INIT_POOLED_BUF); assert_eq!(bb.shared(), 0); const NUM: usize = MAX_POOLED_BUF + 1; let mut bufs = Vec::with_capacity(NUM); for i in 1..=NUM { let buf = bb.acquire(); assert_eq!(bb.acquired(), i); assert_eq!(bb.shared(), 0); assert_eq!(bb.pooled(), INIT_POOLED_BUF.saturating_sub(i)); bufs.push(buf); } assert_eq!(bb.acquired(), NUM); assert_eq!(bb.shared(), 0); assert_eq!(bb.pooled(), 0); let mut shared = Vec::with_capacity(NUM); for i in 1..=NUM { let arc = bb.release_to_share(bufs.pop().unwrap()); assert_eq!(bb.acquired(), NUM - i); assert_eq!(bb.shared(), i); assert_eq!(bb.pooled(), 0); shared.push(arc); } assert_eq!(bb.acquired(), 0); assert_eq!(bb.shared(), NUM); assert_eq!(bb.pooled(), 0); for i in 1..=NUM { drop(shared.pop().unwrap()); let buf = bb.acquire(); assert_eq!(bb.acquired(), i); assert_eq!(bb.shared(), NUM - i); assert_eq!(bb.pooled(), 0); bufs.push(buf); } assert_eq!(bb.acquired(), NUM); assert_eq!(bb.shared(), 0); assert_eq!(bb.pooled(), 0); for i in 1..=NUM { bb.release(bufs.pop().unwrap()); assert_eq!(bb.acquired(), NUM - i); assert_eq!(bb.shared(), 0); assert_eq!(bb.pooled(), min(MAX_POOLED_BUF, i)); } assert_eq!(bb.acquired(), 0); assert_eq!(bb.shared(), 0); assert_eq!(bb.pooled(), min(MAX_POOLED_BUF, NUM)); } } nom-exif-2.5.4/src/ebml/element.rs000064400000000000000000000155351046102023000150740ustar 00000000000000use std::{ fmt::Debug, io::{BufRead, Cursor, Read}, }; use bytes::Buf; use thiserror::Error; use crate::ebml::vint::VInt; use super::vint::ParseVIntFailed; #[derive(Debug, Error)] pub enum ParseEBMLFailed { #[error("need more bytes: {0}")] Need(usize), #[error("not an EBML file")] NotEBMLFile, #[error("invalid EBML file: {0}")] InvalidEBMLFile(Box), } impl From for crate::Error { fn from(e: ParseEBMLFailed) -> Self { match e { ParseEBMLFailed::Need(_) => Self::ParseFailed("no enough bytes".into()), ParseEBMLFailed::NotEBMLFile => Self::ParseFailed(e.into()), ParseEBMLFailed::InvalidEBMLFile(e) => Self::ParseFailed(e), } } } impl From for ParseEBMLFailed { fn from(value: ParseVIntFailed) -> Self { match value { ParseVIntFailed::InvalidVInt(e) => ParseEBMLFailed::InvalidEBMLFile(e.into()), ParseVIntFailed::Need(i) => ParseEBMLFailed::Need(i), } } } pub(crate) const INVALID_ELEMENT_ID: u8 = 0xFF; #[derive(Debug, Clone, Copy)] pub(crate) enum TopElementId { Ebml = 0x1A45DFA3, Segment = 0x18538067, } impl TopElementId { fn code(self) -> u32 { self as u32 } } #[derive(Debug, Error)] #[error("unknown ebml ID: {0}")] pub struct UnknowEbmlIDError(pub u64); impl TryFrom for TopElementId { type Error = UnknowEbmlIDError; fn try_from(v: u64) -> Result { let id = match v { x if x == TopElementId::Ebml.code() as u64 => TopElementId::Ebml, x if x == TopElementId::Segment.code() as u64 => TopElementId::Segment, o => return Err(UnknowEbmlIDError(o)), }; Ok(id) } } #[allow(unused)] #[derive(Debug, Clone, Copy)] enum EBMLHeaderId { Version = 0x4286, ReadVersion = 0x42F7, MaxIdlength = 0x42F2, MaxSizeLength = 0x42F3, DocType = 0x4282, DocTypeVersion = 0x4287, DocTypeReadVersion = 0x4285, DocTypeExtension = 0x4281, DocTypeExtensionName = 0x4283, DocTypeExtensionVersion = 0x4284, } /// These extra elements apply only to the EBML Body, not the EBML Header. pub(crate) enum EBMLGlobalId { Crc32 = 0xBF, Void = 0xEC, } /// Refer to [EBML header /// elements](https://github.com/ietf-wg-cellar/ebml-specification/blob/master/specification.markdown#ebml-header-elements) pub(crate) fn parse_ebml_doc_type(cursor: &mut Cursor<&[u8]>) -> Result { let header = next_element_header(cursor)?; tracing::debug!(ebml_header = ?header); if header.id != TopElementId::Ebml as u64 { return Err(ParseEBMLFailed::NotEBMLFile); } if cursor.remaining() < header.data_size { return Err(ParseEBMLFailed::Need(header.data_size - cursor.remaining())); } let pos = cursor.position() as usize; // consume all header data cursor.consume(header.data_size); // get doc type match parse_ebml_head_data(&cursor.get_ref()[pos..pos + header.data_size]) { Ok(x) => Ok(x), // Don't bubble Need error to caller here Err(ParseEBMLFailed::Need(_)) => Err(ParseEBMLFailed::NotEBMLFile), Err(e) => Err(e), } } fn parse_ebml_head_data(input: &[u8]) -> Result { let mut cur = Cursor::new(input); while cur.has_remaining() { let h = next_element_header(&mut cur)?; if h.id == EBMLHeaderId::DocType as u64 { let s = get_cstr(&mut cur, h.data_size) .ok_or_else(|| ParseEBMLFailed::Need(h.data_size - cur.remaining()))?; return Ok(s); } if cur.remaining() < h.data_size { return Err(ParseEBMLFailed::Need(h.data_size - cur.remaining())); } cur.consume(h.data_size); } Err(ParseEBMLFailed::NotEBMLFile) } pub(crate) fn find_element_by_id( cursor: &mut Cursor<&[u8]>, target_id: u64, ) -> Result { while cursor.has_remaining() { let header = next_element_header(cursor)?; if header.id == target_id { return Ok(header); } if cursor.remaining() < header.data_size { return Err(ParseEBMLFailed::Need(header.data_size - cursor.remaining())); } cursor.consume(header.data_size); } Err(ParseEBMLFailed::Need(1)) } pub(crate) fn travel_while( cursor: &mut Cursor<&[u8]>, mut predict: F, ) -> Result where F: FnMut(&ElementHeader) -> bool, { while cursor.has_remaining() { let header = next_element_header(cursor)?; if !predict(&header) { return Ok(header); } if cursor.remaining() < header.data_size { return Err(ParseEBMLFailed::Need(header.data_size - cursor.remaining())); } cursor.consume(header.data_size); } Err(ParseEBMLFailed::Need(1)) } #[derive(Clone)] pub(crate) struct ElementHeader { pub id: u64, pub data_size: usize, pub header_size: usize, } pub(crate) fn next_element_header( cursor: &mut Cursor<&[u8]>, ) -> Result { let pos = cursor.position() as usize; let id = VInt::as_u64_with_marker(cursor)?; let data_size = VInt::as_usize(cursor)?; let header_size = cursor.position() as usize - pos; Ok(ElementHeader { id, data_size, header_size, }) } fn get_cstr(cursor: &mut Cursor<&[u8]>, size: usize) -> Option { if cursor.remaining() < size { return None; } let it = Iterator::take(cursor.chunk().iter(), size); let s = it .take_while(|b| **b != 0) .map(|b| (*b) as char) .collect::(); cursor.consume(size); Some(s) } pub(crate) fn get_as_u64(cursor: &mut Cursor<&[u8]>, size: usize) -> Option { if cursor.remaining() < size { return None; } let n = match size { 1 => cursor.get_u8() as u64, 2 => cursor.get_u16() as u64, 3 => { let bytes = [0, cursor.get_u8(), cursor.get_u8(), cursor.get_u8()]; u32::from_be_bytes(bytes) as u64 } 4 => cursor.get_u32() as u64, 5..=8 => { let mut buf = [0u8; 8]; cursor.read_exact(&mut buf[8 - size..]).ok()?; u64::from_be_bytes(buf) } _ => return None, }; Some(n) } pub(crate) fn get_as_f64(cursor: &mut Cursor<&[u8]>, size: usize) -> Option { if cursor.remaining() < size { return None; } let n = match size { 4 => { let buf = [0u8; 4]; f32::from_be_bytes(buf) as f64 } 5..=8 => { let mut buf = [0u8; 8]; cursor.read_exact(&mut buf[8 - size..]).ok()?; f64::from_be_bytes(buf) } _ => return None, }; Some(n) } nom-exif-2.5.4/src/ebml/vint.rs000064400000000000000000000061651046102023000144220ustar 00000000000000use std::io::Cursor; use bytes::Buf; use thiserror::Error; #[derive(Debug)] pub(crate) struct VInt; #[derive(Debug, Error)] pub(crate) enum ParseVIntFailed { #[error("invalid VInt: {0}")] InvalidVInt(&'static str), #[error("need more bytes: {0}")] Need(usize), } impl VInt { pub fn as_u64_with_marker(data: &mut Cursor<&[u8]>) -> Result { let (remain, v) = Self::parse_unsigned(&data.get_ref()[data.position() as usize..], true)?; data.set_position(data.position() + (data.remaining() - remain.len()) as u64); Ok(v) } pub fn as_usize(data: &mut Cursor<&[u8]>) -> Result { let (remain, v) = Self::parse_unsigned(&data.get_ref()[data.position() as usize..], false) .map(|(d, v)| (d, v as usize))?; data.set_position(data.position() + (data.remaining() - remain.len()) as u64); Ok(v) } pub(crate) fn parse_unsigned( data: &[u8], reserve_marker: bool, ) -> Result<(&[u8], u64), ParseVIntFailed> { if data.is_empty() { return Err(ParseVIntFailed::Need(1)); } let n = data[0].leading_zeros() as usize + 1; if n > data.len() { return Err(ParseVIntFailed::Need(n - data.len())); } if n > 8 { return Err(ParseVIntFailed::InvalidVInt("size > 8 is not supported")); } // println!("n: {n}"); let mut octets = [0u8; 8]; let start = 8 - n; octets[start..].copy_from_slice(&data[..n]); // remove the marker if !reserve_marker { if n == 8 { octets[0] = 0; } else { // println!("first byte: {:08b}", data[0]); let first = data[0] & (0xFF >> n); // println!("first byte: {:08b}", first); octets[start] = first; } } let v = u64::from_be_bytes(octets); Ok((&data[n..], v)) } } #[cfg(test)] mod tests { use super::*; use test_case::test_case; #[test_case(&[0b1000_0010], Some((&[], 2)))] #[test_case(&[0b0100_0000, 0b0000_0010], Some((&[], 2)))] #[test_case(&[0b0010_0000, 0b0000_0000, 0b0000_0010], Some((&[], 2)))] #[test_case(&[0b0001_0000, 0b0000_0000, 0b0000_0000, 0b0000_0010], Some((&[], 2)))] #[test_case(&[0b0001_0000, 0b0000_0000, 0b1000_0000, 0b0000_0000, 0xFF], Some((&[0xFF], 0x8000)))] #[test_case(&[0b0000_0001, 0b1000_0000, 0b1000_0000, 0b0000_0001], None)] #[test_case(&[0b0000_0010, 0b1000_1000, 0b1000_1000, 0b0000_0000, 0, 0, 0x80, 0x08], Some((&[0x08], 0x0000_8888_0000_0080)))] #[test_case(&[0b0000_0001, 0b1000_1000, 0b1000_1000, 0b0000_0000, 0, 0, 0x80, 0x08], Some((&[], 0x0088_8800_0000_8008)))] #[test_case(&[0b0000_0001, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], Some((&[], 0x00ff_ffff_ffff_ffff)))] fn vint_parse_u(data: &[u8], expect: Option<(&[u8], u64)>) { let actual = VInt::parse_unsigned(data, false); if let Some(expect) = expect { assert_eq!(actual.unwrap(), expect); } else { actual.unwrap_err(); } } } nom-exif-2.5.4/src/ebml/webm.rs000064400000000000000000000512121046102023000143650ustar 00000000000000use std::{ collections::HashMap, fmt::Debug, io::{BufRead, Cursor}, }; use bytes::Buf; use chrono::{DateTime, NaiveDate, Utc}; use nom::{error::ErrorKind, multi::many_till}; use thiserror::Error; use crate::{ ebml::element::{ find_element_by_id, get_as_f64, get_as_u64, next_element_header, parse_ebml_doc_type, EBMLGlobalId, TopElementId, }, error::ParsingError, video::{TrackInfo, TrackInfoTag}, }; use super::{ element::{ travel_while, ElementHeader, ParseEBMLFailed, UnknowEbmlIDError, INVALID_ELEMENT_ID, }, vint::{ParseVIntFailed, VInt}, }; #[derive(Debug, Clone, Default)] pub struct EbmlFileInfo { #[allow(unused)] doc_type: String, segment_info: SegmentInfo, tracks_info: TracksInfo, } impl From for TrackInfo { fn from(value: EbmlFileInfo) -> Self { let mut info = TrackInfo::default(); if let Some(date) = value.segment_info.date { info.put(TrackInfoTag::CreateDate, date.into()); } info.put( TrackInfoTag::DurationMs, ((value.segment_info.duration / 1000.0 / 1000.0) as u64).into(), ); info.put(TrackInfoTag::ImageWidth, value.tracks_info.width.into()); info.put(TrackInfoTag::ImageHeight, value.tracks_info.height.into()); info } } #[derive(Debug, Error)] pub enum ParseWebmFailed { #[error("need more bytes: {0}")] Need(usize), #[error("not an WEBM file")] NotWebmFile, #[error("invalid WEBM file: {0}")] InvalidWebmFile(Box), #[error("invalid seek entry")] InvalidSeekEntry, } /// Parse EBML based files, e.g.: `.webm`, `.mkv`, etc. /// /// Refer to: /// - [Matroska Elements](https://www.matroska.org/technical/elements.html) /// - [EBML Specification](https://github.com/ietf-wg-cellar/ebml-specification/blob/master/specification.markdown) #[tracing::instrument(skip_all)] pub(crate) fn parse_webm(input: &[u8]) -> Result { let (doc_type, pos) = { let mut cursor = Cursor::new(input); let doc_type = parse_ebml_doc_type(&mut cursor)?; (doc_type, cursor.position() as usize) }; tracing::debug!(doc_type, pos); let pos = { let mut cursor = Cursor::new(&input[pos..]); let header = next_element_header(&mut cursor)?; tracing::debug!(segment_header = ?header); if header.id != TopElementId::Segment as u64 { return Err(ParseWebmFailed::NotWebmFile.into()); } pos + cursor.position() as usize }; let mut file_info = EbmlFileInfo { doc_type, ..Default::default() }; let mut info_set = false; let mut tracks_set = false; if let Ok(seeks) = parse_seeks(input, pos) { let info_seek = seeks.get(&(SegmentId::Info as u32)).cloned(); let tracks_seek = seeks.get(&(SegmentId::Tracks as u32)).cloned(); if let Some(pos) = info_seek { let info = parse_segment_info(input, pos as usize)?; tracing::debug!(?info); if let Some(info) = info { info_set = true; file_info.segment_info = info; } } if let Some(pos) = tracks_seek { let tracks = parse_tracks_info(input, pos as usize)?; tracing::debug!(?tracks); if let Some(info) = tracks { tracks_set = true; file_info.tracks_info = info; } } } if !info_set { // According to the specification, The first Info Element SHOULD occur // before the first Tracks Element let info: Option = { let mut cursor = Cursor::new(&input[pos..]); let header = travel_while(&mut cursor, |h| h.id != SegmentId::Info as u64)?; parse_segment_info( &input[pos + cursor.position() as usize - header.header_size..], 0, ) }?; tracing::debug!(?info); if let Some(info) = info { file_info.segment_info = info; } } if !tracks_set { let track = { let mut cursor = Cursor::new(&input[pos..]); let header = travel_while(&mut cursor, |h| h.id != SegmentId::Tracks as u64)?; parse_tracks_info( &input[pos + cursor.position() as usize - header.header_size..], 0, )? }; tracing::debug!(?track); if let Some(info) = track { file_info.tracks_info = info; } } Ok(file_info) } #[derive(Debug, Clone, Default)] struct TracksInfo { width: u32, height: u32, } #[tracing::instrument(skip(input))] fn parse_tracks_info(input: &[u8], pos: usize) -> Result, ParseWebmFailed> { if pos >= input.len() { return Err(ParseWebmFailed::Need(pos - input.len() + 1)); } let mut cursor = Cursor::new(&input[pos..]); let header = next_element_header(&mut cursor)?; tracing::debug!(tracks_info_header = ?header); if cursor.remaining() < header.data_size { return Err(ParseWebmFailed::Need(header.data_size - cursor.remaining())); } const Z: &[u8] = &[]; let start = pos + cursor.position() as usize; let data = &input[start..start + header.data_size]; if let Ok((_, (_, track))) = many_till::<&[u8], (), Option<_>, (&[u8], ErrorKind), _, _>( |data| { let mut cursor = Cursor::new(data); let header = next_element_header(&mut cursor)?; cursor.consume(std::cmp::min(cursor.remaining(), header.data_size)); Ok((&data[cursor.position() as usize..], ())) }, |data| { let mut cursor = Cursor::new(data); let header = next_element_header(&mut cursor)?; tracing::debug!(tracks_sub_track_entry = ?header); if header.id != TracksId::TrackEntry as u64 { return Err(nom::Err::Error((Z, ErrorKind::Fail))); }; if cursor.remaining() < header.data_size { return Err(nom::Err::Error((Z, ErrorKind::Fail))); } let track = parse_track(&cursor.chunk()[..header.data_size]).map(|x| { x.map(|x| TracksInfo { width: x.width, height: x.height, }) })?; Ok((Z, track)) }, )(data) { Ok(track) } else { Ok(None) } // let mut cursor = Cursor::new(&cursor.chunk()[..header.data_size]); // let header = match travel_while(&mut cursor, |h| h.id != TracksId::VideoTrack as u64) { // Ok(x) => x, // // Don't bubble Need error to caller here // Err(ParseEBMLFailed::Need(_)) => return Ok(None), // Err(e) => return Err(e.into()), // }; // tracing::debug!(?header, "video track"); // if cursor.remaining() < header.data_size { // return Err(ParseWebmFailed::Need(header.data_size - cursor.remaining())); // } // match parse_track(&cursor.chunk()[..header.data_size]).map(|x| { // x.map(|x| TracksInfo { // width: x.width, // height: x.height, // }) // }) { // Ok(x) => Ok(x), // // Don't bubble Need error to caller here // Err(ParseWebmFailed::Need(_)) => Ok(None), // Err(e) => Err(e), // } } fn parse_track(input: &[u8]) -> Result, ParseWebmFailed> { let mut cursor = Cursor::new(input); while cursor.has_remaining() { let header = next_element_header(&mut cursor)?; tracing::debug!(?header, "track sub-element"); let id = TryInto::::try_into(header.id); let pos = cursor.position() as usize; cursor.consume(header.data_size); let Ok(id) = id else { continue; }; if id == TracksId::VideoTrack { let end = pos + header.data_size; if end > input.len() { tracing::warn!( ?pos, end = pos + header.data_size, input_len = input.len(), "invalid track sub-element" ); continue; } // Safe-slice return parse_video_track(&input[pos..pos + header.data_size]); } } Ok(None) } fn parse_video_track(input: &[u8]) -> Result, ParseWebmFailed> { let mut cursor = Cursor::new(input); let mut info = VideoTrackInfo::default(); let header = travel_while(&mut cursor, |h| h.id != TracksId::PixelWidth as u64)?; tracing::debug!(?header, "video track width element"); if let Some(v) = get_as_u64(&mut cursor, header.data_size) { info.width = v as u32; } // search from beginning cursor.set_position(0); let header = travel_while(&mut cursor, |h| h.id != TracksId::PixelHeight as u64)?; tracing::debug!(?header, "video track height element"); if let Some(v) = get_as_u64(&mut cursor, header.data_size) { info.height = v as u32; } if info == VideoTrackInfo::default() { Ok(None) } else { Ok(Some(info)) } } #[derive(Debug, Clone, Default, PartialEq, Eq)] struct VideoTrackInfo { width: u32, height: u32, } #[derive(Debug, Clone, Default)] struct SegmentInfo { // in nano seconds duration: f64, date: Option>, } #[tracing::instrument(skip(input))] fn parse_segment_info(input: &[u8], pos: usize) -> Result, ParsingError> { if pos >= input.len() { return Err(ParsingError::Need(pos - input.len() + 1)); } let mut cursor = Cursor::new(&input[pos..]); let header = next_element_header(&mut cursor)?; tracing::debug!(segment_info_header = ?header); if cursor.remaining() < header.data_size { return Err(ParsingError::Need(header.data_size - cursor.remaining())); } let mut cursor = Cursor::new(&cursor.chunk()[..header.data_size]); match parse_segment_info_body(&mut cursor) { Ok(x) => Ok(Some(x)), // Don't bubble Need error to caller here Err(ParsingError::Need(_)) => Ok(None), Err(e) => Err(e), } } fn parse_segment_info_body(cursor: &mut Cursor<&[u8]>) -> Result { // timestamp in nanosecond = element value * TimestampScale // By default, one segment tick represents one millisecond let mut time_scale = 1_000_000; let mut info = SegmentInfo::default(); while cursor.has_remaining() { let header = next_element_header(cursor)?; let id = TryInto::::try_into(header.id); tracing::debug!(?header, "segment info sub-element"); if let Ok(id) = id { match id { InfoId::TimestampScale => { if let Some(v) = get_as_u64(cursor, header.data_size) { time_scale = v; } } InfoId::Duration => { if let Some(v) = get_as_f64(cursor, header.data_size) { info.duration = v * time_scale as f64; } } InfoId::Date => { if let Some(v) = get_as_u64(cursor, header.data_size) { // webm date is a 2001 based timestamp let dt = NaiveDate::from_ymd_opt(2001, 1, 1) .unwrap() .and_hms_opt(0, 0, 0) .unwrap() .and_utc(); let diff = dt - DateTime::from_timestamp_nanos(0); info.date = Some(DateTime::from_timestamp_nanos(v as i64) + diff); } } } } else { cursor.consume(header.data_size); } } Ok(info) } fn parse_seeks(input: &[u8], pos: usize) -> Result, ParsingError> { let mut cursor = Cursor::new(&input[pos..]); // find SeekHead element let header = find_element_by_id(&mut cursor, SegmentId::SeekHead as u64)?; tracing::debug!(segment_header = ?header); if cursor.remaining() < header.data_size { return Err(ParsingError::Need(header.data_size - cursor.remaining())); } let header_pos = pos + cursor.position() as usize - header.header_size; let mut cur = Cursor::new(&cursor.chunk()[..header.data_size]); let mut seeks = parse_seek_head(&mut cur)?; for (_, pos) in seeks.iter_mut() { *pos += header_pos as u64; } Ok(seeks) } #[derive(Clone)] struct SeekEntry { seek_id: u32, seek_pos: u64, } impl Debug for SeekEntry { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let id = self.seek_id as u64; let s = TryInto::::try_into(id) .map(|x| format!("{x:?}")) .or_else(|_| TryInto::::try_into(id).map(|x| format!("{x:?}"))) .unwrap_or_else(|_| format!("0x{:04x}", id)); f.debug_struct("SeekEntry") .field("seekId", &s) .field("seekPosition", &self.seek_pos.to_string()) .finish() } } #[tracing::instrument(skip_all)] fn parse_seek_head(input: &mut Cursor<&[u8]>) -> Result, ParseWebmFailed> { let mut entries = HashMap::new(); while input.has_remaining() { match parse_seek_entry(input) { Ok(Some(entry)) => { tracing::debug!(seek_entry=?entry); entries.insert(entry.seek_id, entry.seek_pos); } Ok(None) => { // tracing::debug!("Void or Crc32 Element"); } Err(ParseWebmFailed::InvalidSeekEntry) => { tracing::debug!("ignore invalid seek entry"); } Err(e) => return Err(e), }; } Ok(entries) } fn parse_seek_entry(input: &mut Cursor<&[u8]>) -> Result, ParseWebmFailed> { // 0xFF is an invalid ID let mut seek_id = INVALID_ELEMENT_ID as u32; let mut seek_pos = 0u64; let id = VInt::as_u64_with_marker(input)?; let data_size = VInt::as_usize(input)?; if input.remaining() < data_size { return Err(ParseWebmFailed::Need(data_size - input.remaining())); } if id != SeekHeadId::Seek as u64 { input.consume(data_size); if id == EBMLGlobalId::Crc32 as u64 || id == EBMLGlobalId::Void as u64 { return Ok(None); } tracing::debug!( id = format!("0x{id:x}"), "invalid seek entry: id != 0x{:x}", SeekHeadId::Seek as u32 ); return Err(ParseWebmFailed::InvalidSeekEntry); } let pos = input.position() as usize; input.consume(data_size); let mut buf = Cursor::new(&input.get_ref()[pos..pos + data_size]); while buf.has_remaining() { let id = VInt::as_u64_with_marker(&mut buf)?; let size = VInt::as_usize(&mut buf)?; match id { x if x == SeekHeadId::SeekId as u64 => { seek_id = VInt::as_u64_with_marker(&mut buf)? as u32; } x if x == SeekHeadId::SeekPosition as u64 => { seek_pos = get_as_u64(&mut buf, size).ok_or_else(|| ParseWebmFailed::InvalidSeekEntry)?; } _ => { tracing::debug!(id = format!("0x{id:x}"), "invalid seek entry"); return Err(ParseWebmFailed::InvalidSeekEntry); } } if seek_id != INVALID_ELEMENT_ID as u32 && seek_pos != 0 { break; } } if seek_id == INVALID_ELEMENT_ID as u32 || seek_pos == 0 { return Err(ParseWebmFailed::InvalidSeekEntry); } Ok(Some(SeekEntry { seek_id, seek_pos })) } #[derive(Debug, Clone, Copy)] enum SegmentId { SeekHead = 0x114D9B74, Info = 0x1549A966, Tracks = 0x1654AE6B, Cluster = 0x1F43B675, Cues = 0x1C53BB6B, } #[derive(Debug, Clone, Copy)] enum InfoId { TimestampScale = 0x2AD7B1, Duration = 0x4489, Date = 0x4461, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum TracksId { TrackEntry = 0xAE, TrackType = 0x83, VideoTrack = 0xE0, PixelWidth = 0xB0, PixelHeight = 0xBA, } impl TryFrom for TracksId { type Error = UnknowEbmlIDError; fn try_from(v: u64) -> Result { let id = match v { x if x == Self::TrackEntry as u64 => Self::TrackEntry, x if x == Self::TrackType as u64 => Self::TrackType, x if x == Self::VideoTrack as u64 => Self::VideoTrack, x if x == Self::PixelWidth as u64 => Self::PixelWidth, x if x == Self::PixelHeight as u64 => Self::PixelHeight, o => return Err(UnknowEbmlIDError(o)), }; Ok(id) } } impl TryFrom for InfoId { type Error = UnknowEbmlIDError; fn try_from(v: u64) -> Result { let id = match v { x if x == Self::TimestampScale as u64 => Self::TimestampScale, x if x == Self::Duration as u64 => Self::Duration, x if x == Self::Date as u64 => Self::Date, o => return Err(UnknowEbmlIDError(o)), }; Ok(id) } } #[derive(Debug, Clone, Copy)] enum SeekHeadId { Seek = 0x4DBB, SeekId = 0x53AB, SeekPosition = 0x53AC, } impl TryFrom for SegmentId { type Error = UnknowEbmlIDError; fn try_from(v: u64) -> Result { let id = match v { x if x == Self::SeekHead as u64 => Self::SeekHead, x if x == Self::Info as u64 => Self::Info, x if x == Self::Tracks as u64 => Self::Tracks, x if x == Self::Cluster as u64 => Self::Cluster, x if x == Self::Cues as u64 => Self::Cues, o => return Err(UnknowEbmlIDError(o)), }; Ok(id) } } impl Debug for ElementHeader { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let s = TryInto::::try_into(self.id) .map(|x| format!("{x:?}")) .or_else(|_| TryInto::::try_into(self.id).map(|x| format!("{x:?}"))) .or_else(|_| TryInto::::try_into(self.id).map(|x| format!("{x:?}"))) .or_else(|_| TryInto::::try_into(self.id).map(|x| format!("{x:?}"))) .unwrap_or_else(|_| format!("0x{:04x}", self.id)); f.debug_struct("ElementHeader") .field("id", &s) .field("data_size", &self.data_size.to_string()) .finish() } } impl From for ParseWebmFailed { fn from(value: ParseEBMLFailed) -> Self { match value { ParseEBMLFailed::Need(i) => Self::Need(i), ParseEBMLFailed::NotEBMLFile => Self::NotWebmFile, ParseEBMLFailed::InvalidEBMLFile(e) => Self::InvalidWebmFile(e), } } } impl From for ParsingError { fn from(value: ParseEBMLFailed) -> Self { match value { ParseEBMLFailed::Need(i) => ParsingError::Need(i), ParseEBMLFailed::NotEBMLFile | ParseEBMLFailed::InvalidEBMLFile(_) => { ParsingError::Failed(value.to_string()) } } } } impl From for ParseWebmFailed { fn from(value: ParseVIntFailed) -> Self { match value { ParseVIntFailed::InvalidVInt(e) => Self::InvalidWebmFile(e.into()), ParseVIntFailed::Need(i) => Self::Need(i), } } } impl From for ParsingError { fn from(value: ParseVIntFailed) -> Self { match value { ParseVIntFailed::InvalidVInt(_) => Self::Failed(value.to_string()), ParseVIntFailed::Need(i) => Self::Need(i), } } } impl From for ParsingError { fn from(value: ParseWebmFailed) -> Self { match value { ParseWebmFailed::NotWebmFile | ParseWebmFailed::InvalidWebmFile(_) | ParseWebmFailed::InvalidSeekEntry => Self::Failed(value.to_string()), ParseWebmFailed::Need(n) => Self::Need(n), } } } impl From for nom::Err<(&[u8], ErrorKind)> { fn from(value: ParseEBMLFailed) -> Self { match value { // Don't bubble Need error to caller, since we only use nom for // complete data here. ParseEBMLFailed::Need(_) | ParseEBMLFailed::NotEBMLFile | ParseEBMLFailed::InvalidEBMLFile(_) => nom::Err::Error((&[], ErrorKind::Fail)), } } } impl From for nom::Err<(&[u8], ErrorKind)> { fn from(_: ParseWebmFailed) -> Self { // Don't bubble Need error to caller, since we only use nom for // complete data here. nom::Err::Error((&[], ErrorKind::Fail)) } } nom-exif-2.5.4/src/ebml.rs000064400000000000000000000000701046102023000134270ustar 00000000000000pub(crate) mod element; pub(crate) mod webm; mod vint; nom-exif-2.5.4/src/error.rs000064400000000000000000000151511046102023000136470ustar 00000000000000use std::{ fmt::{Debug, Display}, io::{self}, string::FromUtf8Error, }; use thiserror::Error; type FallbackError = Box; #[derive(Debug, Error)] pub enum Error { #[error("parse failed: {0}")] ParseFailed(FallbackError), #[error("io error: {0}")] IOError(std::io::Error), /// If you encounter this error, please consider filing a bug on github #[error("unrecognized file format")] UnrecognizedFileFormat, } #[derive(Debug, Error)] pub(crate) enum ParsedError { #[error("no enough bytes")] NoEnoughBytes, #[error("io error: {0}")] IOError(std::io::Error), #[error("{0}")] Failed(String), } /// Due to the fact that metadata in MOV files is typically located at the end /// of the file, conventional parsing methods would require reading a /// significant amount of unnecessary data during the parsing process. This /// would impact the performance of the parsing program and consume more memory. /// /// To address this issue, we have defined an `Error::Skip` enumeration type to /// inform the caller that certain bytes in the parsing process are not required /// and can be skipped directly. The specific method of skipping can be /// determined by the caller based on the situation. For example: /// /// - For files, you can quickly skip using a `Seek` operation. /// /// - For network byte streams, you may need to skip these bytes through read /// operations, or preferably, by designing an appropriate network protocol for /// skipping. /// /// # [`ParsingError::Skip`] /// /// Please note that when the caller receives an `Error::Skip(n)` error, it /// should be understood as follows: /// /// - The parsing program has already consumed all available data and needs to /// skip n bytes further. /// /// - After skipping n bytes, it should continue to read subsequent data to fill /// the buffer and use it as input for the parsing function. /// /// - The next time the parsing function is called (usually within a loop), the /// previously consumed data (including the skipped bytes) should be ignored, /// and only the newly read data should be passed in. /// /// # [`ParsingError::Need`] /// /// Additionally, to simplify error handling, we have integrated /// `nom::Err::Incomplete` error into `Error::Need`. This allows us to use the /// same error type to notify the caller that we require more bytes to continue /// parsing. #[derive(Debug, Error)] pub(crate) enum ParsingError { #[error("need more bytes: {0}")] Need(usize), #[error("clear and skip bytes: {0:?}")] ClearAndSkip(usize), #[error("{0}")] Failed(String), } #[derive(Debug, Error)] pub(crate) struct ParsingErrorState { pub err: ParsingError, pub state: Option, } impl ParsingErrorState { pub fn new(err: ParsingError, state: Option) -> Self { Self { err, state } } } impl Display for ParsingErrorState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { Display::fmt( &format!( "ParsingError(err: {}, state: {})", self.err, self.state .as_ref() .map(|x| x.to_string()) .unwrap_or("None".to_string()) ), f, ) } } impl From<&str> for ParsingError { fn from(value: &str) -> Self { Self::Failed(value.to_string()) } } impl From for ParsedError { fn from(value: std::io::Error) -> Self { Self::IOError(value) } } impl From for crate::Error { fn from(value: ParsedError) -> Self { match value { ParsedError::NoEnoughBytes => Self::ParseFailed(value.into()), ParsedError::IOError(e) => Self::IOError(e), ParsedError::Failed(e) => Self::ParseFailed(e.into()), } } } use Error::*; use crate::parser::ParsingState; impl From for Error { fn from(value: io::Error) -> Self { ParseFailed(value.into()) } } impl From for Error { fn from(src: String) -> Error { ParseFailed(src.into()) } } impl From<&str> for Error { fn from(src: &str) -> Error { src.to_string().into() } } impl From for Error { fn from(value: FromUtf8Error) -> Self { ParseFailed(value.into()) } } impl From>> for crate::Error { fn from(e: nom::Err>) -> Self { convert_parse_error(e, "") } } pub(crate) fn convert_parse_error( e: nom::Err>, message: &str, ) -> Error { let s = match e { nom::Err::Incomplete(_) => format!("{e}; {message}"), nom::Err::Error(e) => format!("{}; {message}", e.code.description()), nom::Err::Failure(e) => format!("{}; {message}", e.code.description()), }; s.into() } impl From>> for ParsingError { fn from(e: nom::Err>) -> Self { match e { nom::Err::Incomplete(needed) => match needed { nom::Needed::Unknown => ParsingError::Need(1), nom::Needed::Size(n) => ParsingError::Need(n.get()), }, nom::Err::Failure(e) | nom::Err::Error(e) => { ParsingError::Failed(e.code.description().to_string()) } } } } // impl From>> for ParsingErrorState { // fn from(e: nom::Err>) -> Self { // match e { // nom::Err::Incomplete(needed) => match needed { // nom::Needed::Unknown => ParsingErrorState::new(ParsingError::Need(1), None), // nom::Needed::Size(n) => ParsingErrorState::new(ParsingError::Need(n.get()), None), // }, // nom::Err::Failure(e) | nom::Err::Error(e) => { // ParsingErrorState::new(ParsingError::Failed(e.code.description().to_string()), None) // } // } // } // } pub(crate) fn nom_error_to_parsing_error_with_state( e: nom::Err>, state: Option, ) -> ParsingErrorState { match e { nom::Err::Incomplete(needed) => match needed { nom::Needed::Unknown => ParsingErrorState::new(ParsingError::Need(1), state), nom::Needed::Size(n) => ParsingErrorState::new(ParsingError::Need(n.get()), state), }, nom::Err::Failure(e) | nom::Err::Error(e) => ParsingErrorState::new( ParsingError::Failed(e.code.description().to_string()), state, ), } } nom-exif-2.5.4/src/exif/exif_exif.rs000064400000000000000000000260201046102023000154140ustar 00000000000000use std::fmt::Debug; use nom::{ branch::alt, bytes::streaming::tag, combinator, number::Endianness, sequence, IResult, Needed, }; use crate::{EntryValue, ExifIter, ExifTag, GPSInfo, ParsedExifEntry}; use super::ifd::ParsedImageFileDirectory; /// Represents parsed Exif information, can be converted from an [`ExifIter`] /// like this: `let exif: Exif = iter.into()`. #[derive(Clone, Debug, PartialEq)] pub struct Exif { ifds: Vec, gps_info: Option, } impl Exif { fn new(gps_info: Option) -> Exif { Exif { ifds: Vec::new(), gps_info, } } /// Get entry value for the specified `tag` in ifd0 (the main image). /// /// *Note*: /// /// - The parsing error related to this tag won't be reported by this /// method. Either this entry is not parsed successfully, or the tag does /// not exist in the input data, this method will return None. /// /// - If you want to handle parsing error, please consider to use /// [`ExifIter`]. /// /// - If you have any custom defined tag which does not exist in /// [`ExifTag`], you can always get the entry value by a raw tag code, /// see [`Self::get_by_tag_code`]. /// /// ## Example /// /// ```rust /// use nom_exif::*; /// /// fn main() -> Result<()> { /// let mut parser = MediaParser::new(); /// /// let ms = MediaSource::file_path("./testdata/exif.jpg")?; /// let iter: ExifIter = parser.parse(ms)?; /// let exif: Exif = iter.into(); /// /// assert_eq!(exif.get(ExifTag::Model).unwrap(), &"vivo X90 Pro+".into()); /// Ok(()) /// } pub fn get(&self, tag: ExifTag) -> Option<&EntryValue> { self.get_by_ifd_tag_code(0, tag.code()) } /// Get entry value for the specified `tag` in the specified `ifd`. /// /// `ifd` value range: /// - 0: ifd0 (the main image) /// - 1: ifd1 (thumbnail image) /// /// *Note*: /// /// - The parsing error related to this tag won't be reported by this /// method. Either this entry is not parsed successfully, or the tag does /// not exist in the input data, this method will return None. /// /// - If you want to handle parsing error, please consider to use /// [`ExifIter`]. /// /// ## Example /// /// ```rust /// use nom_exif::*; /// /// fn main() -> Result<()> { /// let mut parser = MediaParser::new(); /// /// let ms = MediaSource::file_path("./testdata/exif.jpg")?; /// let iter: ExifIter = parser.parse(ms)?; /// let exif: Exif = iter.into(); /// /// assert_eq!(exif.get_by_ifd_tag_code(0, 0x0110).unwrap(), &"vivo X90 Pro+".into()); /// assert_eq!(exif.get_by_ifd_tag_code(1, 0xa002).unwrap(), &240_u32.into()); /// Ok(()) /// } /// ``` pub fn get_by_ifd_tag_code(&self, ifd: usize, tag: u16) -> Option<&EntryValue> { self.ifds.get(ifd).and_then(|ifd| ifd.get(tag)) } /// Get entry values for the specified `tags` in ifd0 (the main image). /// /// Please note that this method will ignore errors encountered during the /// search and parsing process, such as missing tags or errors in parsing /// values, and handle them silently. #[deprecated( since = "1.5.0", note = "please use [`Self::get`] or [`ExifIter`] instead" )] pub fn get_values<'b>(&self, tags: &'b [ExifTag]) -> Vec<(&'b ExifTag, EntryValue)> { tags.iter() .zip(tags.iter()) .filter_map(|x| { #[allow(deprecated)] self.get_value(x.0) .map(|v| v.map(|v| (x.0, v))) .unwrap_or(None) }) .collect::>() } /// Get entry value for the specified `tag` in ifd0 (the main image). #[deprecated(since = "1.5.0", note = "please use [`Self::get`] instead")] pub fn get_value(&self, tag: &ExifTag) -> crate::Result> { #[allow(deprecated)] self.get_value_by_tag_code(tag.code()) } /// Get entry value for the specified `tag` in ifd0 (the main image). #[deprecated(since = "1.5.0", note = "please use [`Self::get_by_tag_code`] instead")] pub fn get_value_by_tag_code(&self, tag: u16) -> crate::Result> { Ok(self.get_by_ifd_tag_code(0, tag).map(|x| x.to_owned())) } /// Get parsed GPS information. pub fn get_gps_info(&self) -> crate::Result> { Ok(self.gps_info.clone()) } fn put(&mut self, res: &mut ParsedExifEntry) { while self.ifds.len() < res.ifd_index() + 1 { self.ifds.push(ParsedImageFileDirectory::new()); } if let Some(v) = res.take_value() { self.ifds[res.ifd_index()].put(res.tag_code(), v); } } } impl From for Exif { fn from(iter: ExifIter) -> Self { let gps_info = iter.parse_gps_info().ok().flatten(); let mut exif = Exif::new(gps_info); for mut it in iter { exif.put(&mut it); } exif } } pub(crate) const TIFF_HEADER_LEN: usize = 8; /// TIFF Header #[derive(Clone, PartialEq, Eq)] pub(crate) struct TiffHeader { pub endian: Endianness, pub ifd0_offset: u32, } impl Default for TiffHeader { fn default() -> Self { Self { endian: Endianness::Big, ifd0_offset: 0, } } } impl Debug for TiffHeader { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let endian_str = match self.endian { Endianness::Big => "Big", Endianness::Little => "Little", Endianness::Native => "Native", }; f.debug_struct("TiffHeader") .field("endian", &endian_str) .field("ifd0_offset", &format!("{:#x}", self.ifd0_offset)) .finish() } } pub(crate) const IFD_ENTRY_SIZE: usize = 12; impl TiffHeader { pub fn parse(input: &[u8]) -> IResult<&[u8], TiffHeader> { use nom::number::streaming::{u16, u32}; let (remain, endian) = TiffHeader::parse_endian(input)?; let (_, (_, offset)) = sequence::tuple(( combinator::verify(u16(endian), |magic| *magic == 0x2a), u32(endian), ))(remain)?; let header = Self { endian, ifd0_offset: offset, }; Ok((remain, header)) } pub fn parse_ifd_entry_num(input: &[u8], endian: Endianness) -> IResult<&[u8], u16> { let (remain, num) = nom::number::streaming::u16(endian)(input)?; // Safe-slice if num == 0 { return Ok((remain, 0)); } // 12 bytes per entry let size = (num as usize) .checked_mul(IFD_ENTRY_SIZE) .expect("should fit"); if size > remain.len() { return Err(nom::Err::Incomplete(Needed::new(size - remain.len()))); } Ok((remain, num)) } // pub fn first_ifd<'a>(&self, input: &'a [u8], tag_ids: HashSet) -> IResult<&'a [u8], IFD> { // // ifd0_offset starts from the beginning of Header, so we should // // subtract the header size, which is 8 // let offset = self.ifd0_offset - 8; // // skip to offset // let (_, remain) = take(offset)(input)?; // IFD::parse(remain, self.endian, tag_ids) // } fn parse_endian(input: &[u8]) -> IResult<&[u8], Endianness> { combinator::map(alt((tag("MM"), tag("II"))), |endian_marker| { if endian_marker == b"MM" { Endianness::Big } else { Endianness::Little } })(input) } } pub(crate) fn check_exif_header(data: &[u8]) -> Result>> { tag::<_, _, nom::error::Error<_>>(EXIF_IDENT)(data).map(|_| true) } pub(crate) fn check_exif_header2(i: &[u8]) -> IResult<&[u8], ()> { let (remain, _) = nom::sequence::tuple(( nom::number::complete::be_u32, nom::bytes::complete::tag(EXIF_IDENT), ))(i)?; Ok((remain, ())) } pub(crate) const EXIF_IDENT: &str = "Exif\0\0"; #[cfg(test)] mod tests { use std::io::Read; use std::thread; use crate::partial_vec::PartialVec; use test_case::test_case; use crate::exif::input_into_iter; use crate::jpeg::extract_exif_data; use crate::slice::SubsliceRange; use crate::testkit::{open_sample, read_sample}; use crate::ParsedExifEntry; use super::*; #[test] fn header() { let _ = tracing_subscriber::fmt().with_test_writer().try_init(); let buf = [0x4d, 0x4d, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x08, 0x00]; let (_, header) = TiffHeader::parse(&buf).unwrap(); assert_eq!( header, TiffHeader { endian: Endianness::Big, ifd0_offset: 8, } ); } #[test_case("exif.jpg")] fn exif_iter_gps(path: &str) { let buf = read_sample(path).unwrap(); let (_, data) = extract_exif_data(&buf).unwrap(); let data = data .and_then(|x| buf.subslice_in_range(x)) .map(|x| PartialVec::from_vec_range(buf, x)) .unwrap(); let iter = input_into_iter(data, None).unwrap(); let gps = iter.parse_gps_info().unwrap().unwrap(); assert_eq!(gps.format_iso6709(), "+22.53113+114.02148/"); } #[test_case("exif.jpg")] fn clone_exif_iter_to_thread(path: &str) { let buf = read_sample(path).unwrap(); let (_, data) = extract_exif_data(&buf).unwrap(); let data = data .and_then(|x| buf.subslice_in_range(x)) .map(|x| PartialVec::from_vec_range(buf, x)) .unwrap(); let iter = input_into_iter(data, None).unwrap(); let iter2 = iter.clone(); let mut expect = String::new(); open_sample(&format!("{path}.txt")) .unwrap() .read_to_string(&mut expect) .unwrap(); let jh = thread::spawn(move || iter_to_str(iter2)); let result = iter_to_str(iter); // open_sample_w(&format!("{path}.txt")) // .unwrap() // .write_all(result.as_bytes()) // .unwrap(); assert_eq!(result.trim(), expect.trim()); assert_eq!(jh.join().unwrap().trim(), expect.trim()); } fn iter_to_str(it: impl Iterator) -> String { let ss = it .map(|x| { format!( "ifd{}.{:<32} ยป {}", x.ifd_index(), x.tag() .map(|t| t.to_string()) .unwrap_or_else(|| format!("Unknown(0x{:04x})", x.tag_code())), x.get_result() .map(|v| v.to_string()) .map_err(|e| e.to_string()) .unwrap_or_else(|s| s) ) }) .collect::>(); ss.join("\n") } } nom-exif-2.5.4/src/exif/exif_iter.rs000064400000000000000000000731551046102023000154370ustar 00000000000000use std::{collections::HashSet, fmt::Debug, sync::Arc}; use nom::{number::complete, sequence::tuple}; use thiserror::Error; use crate::{ partial_vec::{AssociatedInput, PartialVec}, slice::SliceChecked, values::{DataFormat, EntryData, IRational, ParseEntryError, URational}, EntryValue, ExifTag, }; use super::{exif_exif::IFD_ENTRY_SIZE, tags::ExifTagCode, GPSInfo, TiffHeader}; /// Parses header from input data, and returns an [`ExifIter`]. /// /// All entries are lazy-parsed. That is, only when you iterate over /// [`ExifIter`] will the IFD entries be parsed one by one. /// /// The one exception is the time zone entries. The method will try to find /// and parse the time zone data first, so we can correctly parse all time /// information in subsequent iterates. #[tracing::instrument] pub(crate) fn input_into_iter( input: impl Into + Debug, state: Option, ) -> crate::Result { let input: PartialVec = input.into(); let header = match state { // header has been parsed, and header has been skipped, input data // is the IFD data Some(header) => header, _ => { // header has not been parsed, input data includes IFD header let (_, header) = TiffHeader::parse(&input[..])?; tracing::debug!( ?header, data_len = format!("{:#x}", input.len()), "TIFF header parsed" ); header } }; let start = header.ifd0_offset as usize; if start > input.len() { return Err(crate::Error::ParseFailed("no enough bytes".into())); } tracing::debug!(?header, offset = start); let mut ifd0 = IfdIter::try_new(0, input.to_owned(), header.to_owned(), start, None)?; let tz = ifd0.find_tz_offset(); ifd0.tz = tz.clone(); let iter: ExifIter = ExifIter::new(input, header, tz, ifd0); tracing::debug!(?iter, "got IFD0"); Ok(iter) } /// An iterator version of [`Exif`](crate::Exif). Use [`ParsedExifEntry`] as /// iterator items. /// /// Clone an `ExifIter` is very cheap, the underlying data is shared /// through `Arc`. /// /// The new cloned `ExifIter`'s iteration index will be reset to the first one. /// /// If you want to convert an `ExifIter` `into` an [`Exif`], you probably want /// to clone the `ExifIter` and use the new cloned one to do the converting. /// Since the original's iteration index may have been modified by /// `Iterator::next()` calls. pub struct ExifIter { // Use Arc to make sure we won't clone the owned data. input: Arc, tiff_header: TiffHeader, tz: Option, ifd0: IfdIter, // Iterating status ifds: Vec, visited_offsets: HashSet, } impl Debug for ExifIter { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ExifIter") .field("data len", &self.input.len()) .field("tiff_header", &self.tiff_header) .field("ifd0", &self.ifd0) .field("state", &self.ifds.first().map(|x| (x.index, x.pos))) .field("ifds num", &self.ifds.len()) .finish_non_exhaustive() } } impl Clone for ExifIter { fn clone(&self) -> Self { self.clone_and_rewind() } } impl ExifIter { pub(crate) fn new( input: impl Into, tiff_header: TiffHeader, tz: Option, ifd0: IfdIter, ) -> ExifIter { let ifds = vec![ifd0.clone()]; ExifIter { input: Arc::new(input.into()), tiff_header, tz, ifd0, ifds, visited_offsets: HashSet::new(), } } /// Clone and rewind the iterator's index. /// /// Clone an `ExifIter` is very cheap, the underlying data is shared /// through Arc. pub fn clone_and_rewind(&self) -> Self { let ifd0 = self.ifd0.clone_and_rewind(); let ifds = vec![ifd0.clone()]; Self { input: self.input.clone(), tiff_header: self.tiff_header.clone(), tz: self.tz.clone(), ifd0, ifds, visited_offsets: HashSet::new(), } } /// Try to find and parse gps information. /// /// Calling this method won't affect the iterator's state. /// /// Returns: /// /// - An `Ok>` if gps info is found and parsed successfully. /// - An `Ok` if gps info is not found. /// - An `Err` if gps info is found but parsing failed. #[tracing::instrument(skip_all)] pub fn parse_gps_info(&self) -> crate::Result> { let mut iter = self.clone_and_rewind(); let Some(gps) = iter.find(|x| { tracing::info!(?x, "find"); x.tag.tag().is_some_and(|t| t == ExifTag::GPSInfo) }) else { tracing::warn!(ifd0 = ?iter.ifds.first(), "GPSInfo not found"); return Ok(None); }; let offset = match gps.get_result() { Ok(v) => { if let Some(offset) = v.as_u32() { offset } else { return Err(EntryError(ParseEntryError::InvalidData( "invalid gps offset".into(), )) .into()); } } Err(e) => return Err(e.clone().into()), }; if offset as usize >= iter.input.len() { return Err(crate::Error::ParseFailed( "GPSInfo offset is out of range".into(), )); } let mut gps_subifd = match IfdIter::try_new( gps.ifd, iter.input.partial(&iter.input[..]), iter.tiff_header, offset as usize, iter.tz.clone(), ) { Ok(ifd0) => ifd0.tag_code(ExifTag::GPSInfo.code()), Err(e) => return Err(e), }; Ok(gps_subifd.parse_gps_info()) } pub(crate) fn to_owned(&self) -> ExifIter { ExifIter::new( self.input.to_vec(), self.tiff_header.clone(), self.tz.clone(), self.ifd0.clone_and_rewind(), ) } } #[derive(Debug, Clone, Error)] #[error("ifd entry error: {0}")] pub struct EntryError(ParseEntryError); impl From for crate::Error { fn from(value: EntryError) -> Self { Self::ParseFailed(value.into()) } } /// Represents a parsed IFD entry. Used as iterator items in [`ExifIter`]. #[derive(Clone)] pub struct ParsedExifEntry { // 0: ifd0, 1: ifd1 ifd: usize, tag: ExifTagCode, res: Option>, } impl ParsedExifEntry { /// Get the IFD index value where this entry is located. /// - 0: ifd0 (main image) /// - 1: ifd1 (thumbnail) pub fn ifd_index(&self) -> usize { self.ifd } /// Get recognized Exif tag of this entry, maybe return `None` if the tag /// is unrecognized. /// /// If you have any custom defined tag which does not exist in [`ExifTag`], /// then you should use [`Self::tag_code`] to get the raw tag code. /// /// **Note**: You can always get the raw tag code via [`Self::tag_code`], /// no matter if it's recognized. pub fn tag(&self) -> Option { match self.tag { ExifTagCode::Tag(t) => Some(t), ExifTagCode::Code(_) => None, } } /// Get the raw tag code of this entry. /// /// In case you have some custom defined tags which doesn't exist in /// [`ExifTag`], you can use this method to get the raw tag code of this /// entry. pub fn tag_code(&self) -> u16 { self.tag.code() } /// Returns true if there is an `EntryValue` in self. /// /// Both of the following situations may cause this method to return false: /// - An error occurred while parsing this entry /// - The value has been taken by calling [`Self::take_value`] or /// [`Self::take_result`] methods. pub fn has_value(&self) -> bool { self.res.as_ref().map(|e| e.is_ok()).is_some_and(|b| b) } /// Get the parsed entry value of this entry. pub fn get_value(&self) -> Option<&EntryValue> { match self.res.as_ref() { Some(Ok(v)) => Some(v), Some(Err(_)) | None => None, } } /// Takes out the parsed entry value of this entry. /// /// If you need to convert this `ExifIter` to an [`crate::Exif`], please /// don't call this method! Otherwise the converted `Exif` is incomplete. /// /// **Note**: This method can only be called once! Once it has been called, /// calling it again always returns `None`. You may want to check it by /// calling [`Self::has_value`] before calling this method. pub fn take_value(&mut self) -> Option { match self.res.take() { Some(v) => v.ok(), None => None, } } /// Get the parsed result of this entry. /// /// Returns: /// /// - If any error occurred while parsing this entry, an /// Err(&[`EntryError`]) is returned. /// /// - Otherwise, an Ok(&[`EntryValue`]) is returned. pub fn get_result(&self) -> Result<&EntryValue, &EntryError> { match self.res { Some(ref v) => v.as_ref(), None => panic!("take result of entry twice"), } } /// Takes out the parsed result of this entry. /// /// If you need to convert this `ExifIter` to an [`crate::Exif`], please /// don't call this method! Otherwise the converted `Exif` is incomplete. /// /// Returns: /// /// - If any error occurred while parsing this entry, an /// Err([`InvalidEntry`](crate::Error::InvalidEntry)) is returned. /// /// - Otherwise, an Ok([`EntryValue`]) is returned. /// /// **Note**: This method can ONLY be called once! If you call it twice, it /// will **panic** directly! pub fn take_result(&mut self) -> Result { match self.res.take() { Some(v) => v, None => panic!("take result of entry twice"), } } fn make_ok(ifd: usize, tag: ExifTagCode, v: EntryValue) -> Self { Self { ifd, tag, res: Some(Ok(v)), } } // fn make_err(ifd: usize, tag: ExifTagCode, e: ParseEntryError) -> Self { // Self { // ifd, // tag, // res: Some(Err(EntryError(e))), // } // } } impl Debug for ParsedExifEntry { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let value = match self.get_result() { Ok(v) => format!("{v}"), Err(e) => format!("{e:?}"), }; f.debug_struct("IfdEntryResult") .field("ifd", &format!("ifd{}", self.ifd)) .field("tag", &self.tag) .field("value", &value) .finish() } } const MAX_IFD_DEPTH: usize = 8; impl Iterator for ExifIter { type Item = ParsedExifEntry; #[tracing::instrument(skip_all)] fn next(&mut self) -> Option { loop { if self.ifds.is_empty() { tracing::debug!(?self, "all IFDs has been parsed"); return None; } if self.ifds.len() > MAX_IFD_DEPTH { self.ifds.clear(); tracing::error!( ifds_depth = self.ifds.len(), "ifd depth is too deep, just go back to ifd0" ); self.ifds.push(self.ifd0.clone_with_state()); } let mut ifd = self.ifds.pop()?; let cur_ifd_idx = ifd.ifd_idx; match ifd.next() { Some((tag_code, entry)) => { tracing::debug!(ifd = ifd.ifd_idx, ?tag_code, "next tag entry"); match entry { IfdEntry::IfdNew(new_ifd) => { // NOTE: new_ifd.offset may smaller than current ifd.offset // if new_ifd.offset <= ifd.offset { // tracing::error!( // ?tag_code, // ?new_ifd, // "bad new SUB-IFD: offset is smaller than current IFD" // ); // continue; // } if new_ifd.offset > 0 { if self.visited_offsets.contains(&new_ifd.offset) { // Ignore repeated ifd parsing to avoid dead looping continue; } self.visited_offsets.insert(new_ifd.offset); } let is_subifd = if new_ifd.ifd_idx == ifd.ifd_idx { // Push the current ifd before enter sub-ifd. self.ifds.push(ifd); tracing::debug!(?tag_code, ?new_ifd, "got new SUB-IFD"); true } else { // Otherwise this is a next ifd. It means that the // current ifd has been parsed, so we don't need to // push it. tracing::debug!("IFD{} parsing completed", cur_ifd_idx); tracing::debug!(?new_ifd, "got new IFD"); false }; let (ifd_idx, offset) = (new_ifd.ifd_idx, new_ifd.offset); self.ifds.push(new_ifd); if is_subifd { // Return sub-ifd as an entry return Some(ParsedExifEntry::make_ok( ifd_idx, tag_code.unwrap(), EntryValue::U32(offset as u32), )); } } IfdEntry::Entry(v) => { let res = Some(ParsedExifEntry::make_ok(ifd.ifd_idx, tag_code.unwrap(), v)); self.ifds.push(ifd); return res; } IfdEntry::Err(e) => { tracing::warn!(?tag_code, ?e, "parse ifd entry error"); // let res = // Some(ParsedExifEntry::make_err(ifd.ifd_idx, tag_code.unwrap(), e)); // return res; self.ifds.push(ifd); continue; } } } None => continue, } } } } #[derive(Clone)] pub(crate) struct IfdIter { ifd_idx: usize, tag_code: Option, // starts from TIFF header input: AssociatedInput, // ifd data offset offset: usize, header: TiffHeader, entry_num: u16, pub tz: Option, // Iterating status index: u16, pos: usize, } impl Debug for IfdIter { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("IfdIter") .field("ifd_idx", &self.ifd_idx) .field("tag", &self.tag_code) .field("data len", &self.input.len()) .field("tz", &self.tz) .field("header", &self.header) .field("entry_num", &self.entry_num) .field("index", &self.index) .field("pos", &self.pos) .finish() } } impl IfdIter { pub fn rewind(&mut self) { self.index = 0; // Skip the first two bytes, which is the entry num self.pos = self.offset + 2; } pub fn clone_and_rewind(&self) -> Self { let mut it = self.clone(); it.rewind(); it } pub fn tag_code_maybe(mut self, code: Option) -> Self { self.tag_code = code.map(|x| x.into()); self } pub fn tag_code(mut self, code: u16) -> Self { self.tag_code = Some(code.into()); self } #[allow(unused)] pub fn tag(mut self, tag: ExifTagCode) -> Self { self.tag_code = Some(tag); self } #[tracing::instrument(skip(input))] pub fn try_new( ifd_idx: usize, input: AssociatedInput, header: TiffHeader, offset: usize, tz: Option, ) -> crate::Result { if input.len() < 2 { return Err(crate::Error::ParseFailed( "ifd data is too small to decode entry num".into(), )); } // should use the complete header data to parse ifd entry num assert!(offset <= input.len()); let ifd_data = input.partial(&input[offset..]); let (_, entry_num) = TiffHeader::parse_ifd_entry_num(&ifd_data, header.endian)?; Ok(Self { ifd_idx, tag_code: None, input, offset, header, entry_num, tz, // Skip the first two bytes, which is the entry num pos: offset + 2, index: 0, }) } fn parse_tag_entry(&self, entry_data: &[u8]) -> Option<(u16, IfdEntry)> { let endian = self.header.endian; let (_, (tag, data_format, components_num, value_or_offset)) = tuple(( complete::u16::<_, nom::error::Error<_>>(endian), complete::u16(endian), complete::u32(endian), complete::u32(endian), ))(entry_data) .ok()?; if tag == 0 { return None; } let df: DataFormat = match data_format.try_into() { Ok(df) => df, Err(e) => { let t: ExifTagCode = tag.into(); tracing::warn!(tag = ?t, ?e, "invalid entry data format"); return Some((tag, IfdEntry::Err(e))); } }; let (tag, res) = self.parse_entry(tag, df, components_num, entry_data, value_or_offset); Some((tag, res)) } fn get_data_pos(&self, value_or_offset: u32) -> usize { // value_or_offset.saturating_sub(self.offset) value_or_offset as usize } fn parse_entry( &self, tag: u16, data_format: DataFormat, components_num: u32, entry_data: &[u8], value_or_offset: u32, ) -> (u16, IfdEntry) { // get component_size according to data format let component_size = data_format.component_size(); // get entry data let size = components_num as usize * component_size; let data = if size <= 4 { &entry_data[8..8 + size] // Safe-slice } else { let start = self.get_data_pos(value_or_offset); let end = start + size; let Some(data) = self.input.slice_checked(start..end) else { tracing::warn!( "entry data overflow, tag: {:04x} start: {:08x} end: {:08x} ifd data len {:08x}", tag, start, end, self.input.len(), ); return (tag, IfdEntry::Err(ParseEntryError::EntrySizeTooBig)); }; data }; if SUBIFD_TAGS.contains(&tag) { if let Some(value) = self.new_ifd_iter(self.ifd_idx, value_or_offset, Some(tag)) { return (tag, value); } } let entry = EntryData { endian: self.header.endian, tag, data, data_format, components_num, }; match EntryValue::parse(&entry, &self.tz) { Ok(v) => (tag, IfdEntry::Entry(v)), Err(e) => (tag, IfdEntry::Err(e)), } } fn new_ifd_iter( &self, ifd_idx: usize, value_or_offset: u32, tag: Option, ) -> Option { let offset = self.get_data_pos(value_or_offset); if offset < self.input.len() { match IfdIter::try_new( ifd_idx, self.input.partial(&self.input[..]), self.header.to_owned(), offset, self.tz.clone(), ) { Ok(iter) => return Some(IfdEntry::IfdNew(iter.tag_code_maybe(tag))), Err(e) => { tracing::warn!(?tag, ?e, "Create next/sub IFD failed"); } } // return ( // tag, // // IfdEntry::Ifd { // // idx: self.ifd_idx, // // offset: value_or_offset, // // }, // IfdEntry::IfdNew(), // ); } None } pub fn find_exif_iter(&self) -> Option { let endian = self.header.endian; // find ExifOffset for i in 0..self.entry_num { let pos = self.pos + i as usize * IFD_ENTRY_SIZE; let (_, tag) = complete::u16::<_, nom::error::Error<_>>(endian)(&self.input[pos..]).ok()?; if tag == ExifTag::ExifOffset.code() { let entry_data = self.input.slice_checked(pos..pos + IFD_ENTRY_SIZE)?; let (_, entry) = self.parse_tag_entry(entry_data)?; match entry { IfdEntry::IfdNew(iter) => return Some(iter), IfdEntry::Entry(_) | IfdEntry::Err(_) => return None, } } } None } pub fn find_tz_offset(&self) -> Option { let iter = self.find_exif_iter()?; let mut offset = None; for entry in iter { let Some(tag) = entry.0 else { continue; }; if tag.code() == ExifTag::OffsetTimeOriginal.code() || tag.code() == ExifTag::OffsetTimeDigitized.code() { return entry.1.as_str().map(|x| x.to_owned()); } else if tag.code() == ExifTag::OffsetTime.code() { offset = entry.1.as_str().map(|x| x.to_owned()); } } offset } // Assume the current ifd is GPSInfo subifd. pub fn parse_gps_info(&mut self) -> Option { let mut gps = GPSInfo::default(); let mut has_data = false; for (tag, entry) in self { let Some(tag) = tag.and_then(|x| x.tag()) else { continue; }; has_data = true; match tag { ExifTag::GPSLatitudeRef => { if let Some(c) = entry.as_char() { gps.latitude_ref = c; } } ExifTag::GPSLongitudeRef => { if let Some(c) = entry.as_char() { gps.longitude_ref = c; } } ExifTag::GPSAltitudeRef => { if let Some(c) = entry.as_u8() { gps.altitude_ref = c; } } ExifTag::GPSLatitude => { if let Some(v) = entry.as_urational_array() { gps.latitude = v.try_into().ok()?; } else if let Some(v) = entry.as_irational_array() { gps.latitude = v.try_into().ok()?; } } ExifTag::GPSLongitude => { if let Some(v) = entry.as_urational_array() { gps.longitude = v.try_into().ok()?; } else if let Some(v) = entry.as_irational_array() { gps.longitude = v.try_into().ok()?; } } ExifTag::GPSAltitude => { if let Some(v) = entry.as_urational() { gps.altitude = *v; } else if let Some(v) = entry.as_irational() { gps.altitude = (*v).into(); } } ExifTag::GPSSpeedRef => { if let Some(c) = entry.as_char() { gps.speed_ref = Some(c); } } ExifTag::GPSSpeed => { if let Some(v) = entry.as_urational() { gps.speed = Some(*v); } else if let Some(v) = entry.as_irational() { gps.speed = Some((*v).into()); } } _ => (), } } if has_data { Some(gps) } else { tracing::warn!("GPSInfo data not found"); None } } fn clone_with_state(&self) -> IfdIter { let mut it = self.clone(); it.index = self.index; it.pos = self.pos; it } } #[derive(Debug)] pub(crate) enum IfdEntry { IfdNew(IfdIter), // ifd index Entry(EntryValue), Err(ParseEntryError), } impl IfdEntry { pub fn as_u8(&self) -> Option { if let IfdEntry::Entry(EntryValue::U8(v)) = self { Some(*v) } else { None } } pub fn as_char(&self) -> Option { if let IfdEntry::Entry(EntryValue::Text(s)) = self { s.chars().next() } else { None } } fn as_irational(&self) -> Option<&IRational> { if let IfdEntry::Entry(EntryValue::IRational(v)) = self { Some(v) } else { None } } fn as_irational_array(&self) -> Option<&Vec> { if let IfdEntry::Entry(EntryValue::IRationalArray(v)) = self { Some(v) } else { None } } fn as_urational(&self) -> Option<&URational> { if let IfdEntry::Entry(EntryValue::URational(v)) = self { Some(v) } else { None } } fn as_urational_array(&self) -> Option<&Vec> { if let IfdEntry::Entry(EntryValue::URationalArray(v)) = self { Some(v) } else { None } } fn as_str(&self) -> Option<&str> { if let IfdEntry::Entry(e) = self { e.as_str() } else { None } } } pub(crate) const SUBIFD_TAGS: &[u16] = &[ExifTag::ExifOffset.code(), ExifTag::GPSInfo.code()]; impl Iterator for IfdIter { type Item = (Option, IfdEntry); #[tracing::instrument(skip(self))] fn next(&mut self) -> Option { tracing::debug!( ifd = self.ifd_idx, index = self.index, entry_num = self.entry_num, offset = format!("{:08x}", self.offset), pos = format!("{:08x}", self.pos), "next IFD entry" ); if self.input.len() < self.pos + IFD_ENTRY_SIZE { return None; } let endian = self.header.endian; if self.index > self.entry_num { return None; } if self.index == self.entry_num { tracing::debug!( self.ifd_idx, self.index, pos = self.pos, "try to get next ifd" ); self.index += 1; // next IFD offset let (_, offset) = complete::u32::<_, nom::error::Error<_>>(endian)(&self.input[self.pos..]).ok()?; if offset == 0 { // IFD parsing completed tracing::debug!(?self, "IFD parsing completed"); return None; } return self .new_ifd_iter(self.ifd_idx + 1, offset, None) .map(|x| (None, x)); } let entry_data = self .input .slice_checked(self.pos..self.pos + IFD_ENTRY_SIZE)?; self.index += 1; self.pos += IFD_ENTRY_SIZE; let (tag, res) = self.parse_tag_entry(entry_data)?; Some((Some(tag.into()), res)) // Safe-slice } } #[cfg(test)] mod tests { use crate::exif::extract_exif_with_mime; use crate::exif::input_into_iter; use crate::file::MimeImage; use crate::slice::SubsliceRange; use crate::testkit::read_sample; use crate::Exif; use test_case::test_case; #[test_case("exif.jpg", "+08:00", "2023-07-09T20:36:33+08:00", MimeImage::Jpeg)] #[test_case("exif-no-tz.jpg", "", "2023-07-09 20:36:33", MimeImage::Jpeg)] #[test_case("broken.jpg", "-", "2014-09-21 15:51:22", MimeImage::Jpeg)] #[test_case("exif.heic", "+08:00", "2022-07-22T21:26:32+08:00", MimeImage::Heic)] #[test_case("tif.tif", "-", "-", MimeImage::Tiff)] #[test_case( "fujifilm_x_t1_01.raf.meta", "-", "2014-01-30 12:49:13", MimeImage::Raf )] fn exif_iter_tz(path: &str, tz: &str, time: &str, img_type: MimeImage) { let buf = read_sample(path).unwrap(); let (data, _) = extract_exif_with_mime(img_type, &buf, None).unwrap(); let subslice_in_range = data.and_then(|x| buf.subslice_in_range(x)).unwrap(); let iter = input_into_iter((buf, subslice_in_range), None).unwrap(); let expect = if tz == "-" { None } else { Some(tz.to_string()) }; assert_eq!(iter.tz, expect); let exif: Exif = iter.into(); let value = exif.get(crate::ExifTag::DateTimeOriginal); if time == "-" { assert!(value.is_none()); } else { let value = value.unwrap(); assert_eq!(value.to_string(), time); } } } nom-exif-2.5.4/src/exif/gps.rs000064400000000000000000000247531046102023000142520ustar 00000000000000use std::str::FromStr; use iso6709parse::ISO6709Coord; use crate::values::{IRational, URational}; /// Represents gps information stored in [`GPSInfo`](crate::ExifTag::GPSInfo) /// subIFD. #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct GPSInfo { /// N, S pub latitude_ref: char, /// degree, minute, second, pub latitude: LatLng, /// E, W pub longitude_ref: char, /// degree, minute, second, pub longitude: LatLng, /// 0: Above Sea Level /// 1: Below Sea Level pub altitude_ref: u8, /// meters pub altitude: URational, /// Speed unit /// - K: kilometers per hour /// - M: miles per hour /// - N: knots pub speed_ref: Option, pub speed: Option, } /// degree, minute, second, #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct LatLng(pub URational, pub URational, pub URational); impl GPSInfo { /// Returns an ISO 6709 geographic point location string such as /// `+48.8577+002.295/`. pub fn format_iso6709(&self) -> String { let latitude = self.latitude.0.as_float() + self.latitude.1.as_float() / 60.0 + self.latitude.2.as_float() / 3600.0; let longitude = self.longitude.0.as_float() + self.longitude.1.as_float() / 60.0 + self.longitude.2.as_float() / 3600.0; let altitude = self.altitude.as_float(); format!( "{}{latitude:08.5}{}{longitude:09.5}{}/", if self.latitude_ref == 'N' { '+' } else { '-' }, if self.longitude_ref == 'E' { '+' } else { '-' }, if self.altitude.0 == 0 { "".to_string() } else { format!( "{}{}CRSWGS_84", if self.altitude_ref == 0 { "+" } else { "-" }, Self::format_float(altitude) ) } ) } fn format_float(f: f64) -> String { if f.fract() == 0.0 { f.to_string() } else { format!("{f:.3}") } } /// Returns an ISO 6709 geographic point location string such as /// `+48.8577+002.295/`. #[deprecated(since = "1.2.3", note = "please use `format_iso6709` instead")] #[allow(clippy::wrong_self_convention)] pub fn to_iso6709(&self) -> String { self.format_iso6709() } } impl From<[(u32, u32); 3]> for LatLng { fn from(value: [(u32, u32); 3]) -> Self { let res: [URational; 3] = value.map(|x| x.into()); res.into() // value // .into_iter() // .map(|x| x.into()) // .collect::>() // .try_into() // .unwrap() } } impl From<[URational; 3]> for LatLng { fn from(value: [URational; 3]) -> Self { Self(value[0], value[1], value[2]) } } impl FromIterator<(u32, u32)> for LatLng { fn from_iter>(iter: T) -> Self { let rationals: Vec = iter.into_iter().take(3).map(|x| x.into()).collect(); assert!(rationals.len() >= 3); rationals.try_into().unwrap() } } impl TryFrom> for LatLng { type Error = crate::Error; fn try_from(value: Vec) -> Result { if value.len() < 3 { Err("convert to LatLng failed; need at least 3 (u32, u32)".into()) } else { Ok(Self(value[0], value[1], value[2])) } } } impl FromIterator for LatLng { fn from_iter>(iter: T) -> Self { let mut values = iter.into_iter(); Self( values.next().unwrap(), values.next().unwrap(), values.next().unwrap(), ) } } impl TryFrom<&Vec> for LatLng { type Error = crate::Error; fn try_from(value: &Vec) -> Result { if value.len() < 3 { Err(crate::Error::ParseFailed("invalid URational data".into())) } else { Ok(Self(value[0], value[1], value[2])) } } } impl TryFrom<&Vec> for LatLng { type Error = crate::Error; fn try_from(value: &Vec) -> Result { if value.len() < 3 { Err(crate::Error::ParseFailed("invalid URational data".into())) } else { Ok(Self(value[0].into(), value[1].into(), value[2].into())) } } } pub struct InvalidISO6709Coord; impl FromStr for GPSInfo { type Err = InvalidISO6709Coord; fn from_str(s: &str) -> Result { let info: Self = iso6709parse::parse(s).map_err(|_| InvalidISO6709Coord)?; Ok(info) } } impl From for GPSInfo { fn from(v: ISO6709Coord) -> Self { // let latitude = self.latitude.0.as_float() // + self.latitude.1.as_float() / 60.0 // + self.latitude.2.as_float() / 3600.0; Self { latitude_ref: if v.lat >= 0.0 { 'N' } else { 'S' }, latitude: v.lat.abs().into(), longitude_ref: if v.lon >= 0.0 { 'E' } else { 'W' }, longitude: v.lon.abs().into(), altitude_ref: v .altitude .map(|x| if x >= 0.0 { 0 } else { 1 }) .unwrap_or(0), altitude: v .altitude .map(|x| ((x.abs() * 1000.0).trunc() as u32, 1000).into()) .unwrap_or_default(), ..Default::default() } } } impl From for LatLng { fn from(v: f64) -> Self { let mins = v.fract() * 60.0; [ (v.trunc() as u32, 1), (mins.trunc() as u32, 1), ((mins.fract() * 100.0).trunc() as u32, 100), ] .into() } } // impl> From for LatLng { // fn from(value: T) -> Self { // assert!(value.as_ref().len() >= 3); // value.as_ref().iter().take(3).map(|x| x.into()).collect() // } // } // impl> From for LatLng { // fn from(value: T) -> Self { // assert!(value.as_ref().len() >= 3); // let s = value.as_ref(); // Self(s[0], s[1], s[2]) // } // } #[cfg(test)] mod tests { use crate::values::Rational; use super::*; #[test] fn gps_iso6709() { let _ = tracing_subscriber::fmt().with_test_writer().try_init(); let palace = GPSInfo { latitude_ref: 'N', latitude: LatLng( Rational::(39, 1), Rational::(55, 1), Rational::(0, 1), ), longitude_ref: 'E', longitude: LatLng( Rational::(116, 1), Rational::(23, 1), Rational::(27, 1), ), altitude_ref: 0, altitude: Rational::(0, 1), ..Default::default() }; assert_eq!(palace.format_iso6709(), "+39.91667+116.39083/"); let liberty = GPSInfo { latitude_ref: 'N', latitude: LatLng( Rational::(40, 1), Rational::(41, 1), Rational::(21, 1), ), longitude_ref: 'W', longitude: LatLng( Rational::(74, 1), Rational::(2, 1), Rational::(40, 1), ), altitude_ref: 0, altitude: Rational::(0, 1), ..Default::default() }; assert_eq!(liberty.format_iso6709(), "+40.68917-074.04444/"); let above = GPSInfo { latitude_ref: 'N', latitude: LatLng( Rational::(40, 1), Rational::(41, 1), Rational::(21, 1), ), longitude_ref: 'W', longitude: LatLng( Rational::(74, 1), Rational::(2, 1), Rational::(40, 1), ), altitude_ref: 0, altitude: Rational::(123, 1), ..Default::default() }; assert_eq!(above.format_iso6709(), "+40.68917-074.04444+123CRSWGS_84/"); let below = GPSInfo { latitude_ref: 'N', latitude: LatLng( Rational::(40, 1), Rational::(41, 1), Rational::(21, 1), ), longitude_ref: 'W', longitude: LatLng( Rational::(74, 1), Rational::(2, 1), Rational::(40, 1), ), altitude_ref: 1, altitude: Rational::(123, 1), ..Default::default() }; assert_eq!(below.format_iso6709(), "+40.68917-074.04444-123CRSWGS_84/"); let below = GPSInfo { latitude_ref: 'N', latitude: LatLng( Rational::(40, 1), Rational::(41, 1), Rational::(21, 1), ), longitude_ref: 'W', longitude: LatLng( Rational::(74, 1), Rational::(2, 1), Rational::(40, 1), ), altitude_ref: 1, altitude: Rational::(100, 3), ..Default::default() }; assert_eq!( below.format_iso6709(), "+40.68917-074.04444-33.333CRSWGS_84/" ); } #[test] fn gps_iso6709_with_invalid_alt() { let _ = tracing_subscriber::fmt().with_test_writer().try_init(); let iso: ISO6709Coord = iso6709parse::parse("+26.5322-078.1969+019.099/").unwrap(); assert_eq!(iso.lat, 26.5322); assert_eq!(iso.lon, -78.1969); assert_eq!(iso.altitude, None); let iso: GPSInfo = iso6709parse::parse("+26.5322-078.1969+019.099/").unwrap(); assert_eq!(iso.latitude_ref, 'N'); assert_eq!( iso.latitude, LatLng( Rational::(26, 1), Rational::(31, 1), Rational::(93, 100), ) ); assert_eq!(iso.longitude_ref, 'W'); assert_eq!( iso.longitude, LatLng( Rational::(78, 1), Rational::(11, 1), Rational::(81, 100), ) ); assert_eq!(iso.altitude_ref, 0); assert_eq!( iso.altitude, URational { ..Default::default() } ); } } nom-exif-2.5.4/src/exif/ifd.rs000064400000000000000000000013721046102023000142130ustar 00000000000000use crate::EntryValue; use std::collections::HashMap; /// https://www.media.mit.edu/pia/Research/deepview/exif.html #[derive(Clone, Debug, PartialEq)] pub(crate) struct ParsedImageFileDirectory { pub entries: HashMap, } impl ParsedImageFileDirectory { pub fn new() -> Self { Self { entries: HashMap::new(), } } } #[derive(Clone, Debug, PartialEq)] pub(crate) struct ParsedIdfEntry { pub value: EntryValue, } impl ParsedImageFileDirectory { pub(crate) fn get(&self, tag: u16) -> Option<&EntryValue> { self.entries.get(&tag).map(|x| &x.value) } pub(crate) fn put(&mut self, code: u16, v: EntryValue) { self.entries.insert(code, ParsedIdfEntry { value: v }); } } nom-exif-2.5.4/src/exif/tags.rs000064400000000000000000000540411046102023000144100ustar 00000000000000//! Define exif tags and related enums, see //! https://exiftool.org/TagNames/EXIF.html use std::fmt::{Debug, Display}; #[cfg(feature = "json_dump")] use serde::{Deserialize, Serialize}; #[allow(unused)] #[cfg_attr(feature = "json_dump", derive(Serialize, Deserialize))] #[derive(Eq, PartialEq, Hash, Clone, Copy)] pub(crate) enum ExifTagCode { /// Recognized Exif tag Tag(ExifTag), /// Unrecognized Exif tag Code(u16), } impl ExifTagCode { /// Get recognized Exif tag, maybe return `None` if it's unrecognized. You /// can get raw tag code via [`Self::code`] in this case). pub(crate) fn tag(&self) -> Option { match self { ExifTagCode::Tag(t) => Some(t.to_owned()), ExifTagCode::Code(_) => None, } } /// Get the raw tag code value. pub(crate) fn code(&self) -> u16 { match self { ExifTagCode::Tag(t) => t.code(), ExifTagCode::Code(c) => *c, } } } impl From for ExifTagCode { fn from(v: u16) -> Self { let tag: crate::Result = v.try_into(); if let Ok(tag) = tag { ExifTagCode::Tag(tag) } else { ExifTagCode::Code(v) } } } impl Debug for ExifTagCode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ExifTagCode::Tag(v) => Debug::fmt(v, f), ExifTagCode::Code(v) => Debug::fmt(&format!("Unrecognized(0x{v:04x})"), f), } } } impl Display for ExifTagCode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ExifTagCode::Tag(t) => Display::fmt(t, f), ExifTagCode::Code(c) => Display::fmt(&format!("Unrecognized(0x{c:04x})"), f), } } } /// Defines recognized Exif tags. All tags can be parsed, no matter if it is /// defined here. This enum definition is just for ease of use. /// /// You can always get the entry value by raw tag code which is an `u16` value. /// See [`ParsedExifEntry::tag_code`](crate::ParsedExifEntry::tag_code) and /// [`Exif::get_by_tag_code`](crate::Exif::get_by_tag_code). #[allow(unused)] #[cfg_attr(feature = "json_dump", derive(Serialize, Deserialize))] #[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)] #[non_exhaustive] pub enum ExifTag { Make = 0x0000_010f, Model = 0x0000_0110, Orientation = 0x0000_0112, ImageWidth = 0x0000_0100, ImageHeight = 0x0000_0101, ISOSpeedRatings = 0x0000_8827, ShutterSpeedValue = 0x0000_9201, ExposureTime = 0x0000_829a, FNumber = 0x0000_829d, ExifImageWidth = 0x0000_a002, ExifImageHeight = 0x0000_a003, DateTimeOriginal = 0x0000_9003, CreateDate = 0x0000_9004, ModifyDate = 0x0000_0132, OffsetTime = 0x0000_9010, OffsetTimeOriginal = 0x0000_9011, OffsetTimeDigitized = 0x0000_9012, GPSLatitudeRef = 0x00001, GPSLatitude = 0x00002, GPSLongitudeRef = 0x00003, GPSLongitude = 0x00004, GPSAltitudeRef = 0x00005, GPSAltitude = 0x00006, GPSVersionID = 0x00000, // sub ifd ExifOffset = 0x0000_8769, GPSInfo = 0x0000_8825, ImageDescription = 0x0000_010e, XResolution = 0x0000_011a, YResolution = 0x0000_011b, ResolutionUnit = 0x0000_0128, Software = 0x0000_0131, HostComputer = 0x0000_013c, WhitePoint = 0x0000_013e, PrimaryChromaticities = 0x0000_013f, YCbCrCoefficients = 0x0000_0211, ReferenceBlackWhite = 0x0000_0214, Copyright = 0x0000_8298, ExposureProgram = 0x0000_8822, SpectralSensitivity = 0x0000_8824, OECF = 0x0000_8828, SensitivityType = 0x0000_8830, ExifVersion = 0x0000_9000, ApertureValue = 0x0000_9202, BrightnessValue = 0x0000_9203, ExposureBiasValue = 0x0000_9204, MaxApertureValue = 0x0000_9205, SubjectDistance = 0x0000_9206, MeteringMode = 0x0000_9207, LightSource = 0x0000_9208, Flash = 0x0000_9209, FocalLength = 0x0000_920a, SubjectArea = 0x0000_9214, MakerNote = 0x0000_927c, UserComment = 0x0000_9286, FlashPixVersion = 0x0000_a000, ColorSpace = 0x0000_a001, RelatedSoundFile = 0x0000_a004, FlashEnergy = 0x0000_a20b, FocalPlaneXResolution = 0x0000_a20e, FocalPlaneYResolution = 0x0000_a20f, FocalPlaneResolutionUnit = 0x0000_a210, SubjectLocation = 0x0000_a214, ExposureIndex = 0x0000_a215, SensingMethod = 0x0000_a217, FileSource = 0x0000_a300, SceneType = 0x0000_a301, CFAPattern = 0x0000_a302, CustomRendered = 0x0000_a401, ExposureMode = 0x0000_a402, WhiteBalanceMode = 0x0000_a403, DigitalZoomRatio = 0x0000_a404, FocalLengthIn35mmFilm = 0x0000_a405, SceneCaptureType = 0x0000_a406, GainControl = 0x0000_a407, Contrast = 0x0000_a408, Saturation = 0x0000_a409, Sharpness = 0x0000_a40a, DeviceSettingDescription = 0x0000_a40b, SubjectDistanceRange = 0x0000_a40c, ImageUniqueID = 0x0000_a420, LensSpecification = 0x0000_a432, LensMake = 0x0000_a433, LensModel = 0x0000_a434, Gamma = 0x0000_a500, GPSTimeStamp = 0x00007, GPSSatellites = 0x00008, GPSStatus = 0x00009, GPSMeasureMode = 0x0000a, GPSDOP = 0x0000b, GPSSpeedRef = 0x0000c, GPSSpeed = 0x0000d, GPSTrackRef = 0x0000e, GPSTrack = 0x0000f, GPSImgDirectionRef = 0x0000_0010, GPSImgDirection = 0x0000_0011, GPSMapDatum = 0x0000_0012, GPSDestLatitudeRef = 0x0000_0013, GPSDestLatitude = 0x0000_0014, GPSDestLongitudeRef = 0x0000_0015, GPSDestLongitude = 0x0000_0016, GPSDestBearingRef = 0x0000_0017, GPSDestBearing = 0x0000_0018, GPSDestDistanceRef = 0x0000_0019, GPSDestDistance = 0x0000_001a, GPSProcessingMethod = 0x0000_001b, GPSAreaInformation = 0x0000_001c, GPSDateStamp = 0x0000_001d, GPSDifferential = 0x0000_001e, YCbCrPositioning = 0x0000_0213, RecommendedExposureIndex = 0x0000_8832, SubSecTimeDigitized = 0x0000_9292, SubSecTimeOriginal = 0x0000_9291, SubSecTime = 0x0000_9290, InteropOffset = 0x0000_a005, ComponentsConfiguration = 0x0000_9101, ThumbnailOffset = 0x0000_0201, ThumbnailLength = 0x0000_0202, Compression = 0x0000_0103, BitsPerSample = 0x0000_0102, PhotometricInterpretation = 0x0000_0106, SamplesPerPixel = 0x0000_0115, RowsPerStrip = 0x0000_0116, PlanarConfiguration = 0x0000_011c, } impl ExifTag { pub const fn code(self) -> u16 { self as u16 } } impl Display for ExifTag { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let s: &str = (*self).into(); Display::fmt(s, f) } } impl From for &str { fn from(value: ExifTag) -> Self { match value { ExifTag::Make => "Make", ExifTag::Model => "Model", ExifTag::Orientation => "Orientation", ExifTag::ImageWidth => "ImageWidth", ExifTag::ImageHeight => "ImageHeight", ExifTag::ISOSpeedRatings => "ISOSpeedRatings", ExifTag::ShutterSpeedValue => "ShutterSpeedValue", ExifTag::ExposureTime => "ExposureTime", ExifTag::FNumber => "FNumber", ExifTag::ExifImageWidth => "ExifImageWidth", ExifTag::ExifImageHeight => "ExifImageHeight", ExifTag::DateTimeOriginal => "DateTimeOriginal", ExifTag::CreateDate => "CreateDate", ExifTag::ModifyDate => "ModifyDate", ExifTag::OffsetTime => "OffsetTime", ExifTag::OffsetTimeOriginal => "OffsetTimeOriginal", ExifTag::OffsetTimeDigitized => "OffsetTimeDigitized", ExifTag::GPSLatitudeRef => "GPSLatitudeRef", ExifTag::GPSLatitude => "GPSLatitude", ExifTag::GPSLongitudeRef => "GPSLongitudeRef", ExifTag::GPSLongitude => "GPSLongitude", ExifTag::GPSAltitudeRef => "GPSAltitudeRef", ExifTag::GPSAltitude => "GPSAltitude", ExifTag::GPSVersionID => "GPSVersionID", ExifTag::ExifOffset => "ExifOffset", ExifTag::GPSInfo => "GPSInfo", ExifTag::ImageDescription => "ImageDescription", ExifTag::XResolution => "XResolution", ExifTag::YResolution => "YResolution", ExifTag::ResolutionUnit => "ResolutionUnit", ExifTag::Software => "Software", ExifTag::HostComputer => "HostComputer", ExifTag::WhitePoint => "WhitePoint", ExifTag::PrimaryChromaticities => "PrimaryChromaticities", ExifTag::YCbCrCoefficients => "YCbCrCoefficients", ExifTag::ReferenceBlackWhite => "ReferenceBlackWhite", ExifTag::Copyright => "Copyright", ExifTag::ExposureProgram => "ExposureProgram", ExifTag::SpectralSensitivity => "SpectralSensitivity", ExifTag::OECF => "OECF", ExifTag::SensitivityType => "SensitivityType", ExifTag::ExifVersion => "ExifVersion", ExifTag::ApertureValue => "ApertureValue", ExifTag::BrightnessValue => "BrightnessValue", ExifTag::ExposureBiasValue => "ExposureBiasValue", ExifTag::MaxApertureValue => "MaxApertureValue", ExifTag::SubjectDistance => "SubjectDistance", ExifTag::MeteringMode => "MeteringMode", ExifTag::LightSource => "LightSource", ExifTag::Flash => "Flash", ExifTag::FocalLength => "FocalLength", ExifTag::SubjectArea => "SubjectArea", ExifTag::MakerNote => "MakerNote", ExifTag::UserComment => "UserComment", ExifTag::FlashPixVersion => "FlashPixVersion", ExifTag::ColorSpace => "ColorSpace", ExifTag::RelatedSoundFile => "RelatedSoundFile", ExifTag::FlashEnergy => "FlashEnergy", ExifTag::FocalPlaneXResolution => "FocalPlaneXResolution", ExifTag::FocalPlaneYResolution => "FocalPlaneYResolution", ExifTag::FocalPlaneResolutionUnit => "FocalPlaneResolutionUnit", ExifTag::SubjectLocation => "SubjectLocation", ExifTag::ExposureIndex => "ExposureIndex", ExifTag::SensingMethod => "SensingMethod", ExifTag::FileSource => "FileSource", ExifTag::SceneType => "SceneType", ExifTag::CFAPattern => "CFAPattern", ExifTag::CustomRendered => "CustomRendered", ExifTag::ExposureMode => "ExposureMode", ExifTag::WhiteBalanceMode => "WhiteBalanceMode", ExifTag::DigitalZoomRatio => "DigitalZoomRatio", ExifTag::FocalLengthIn35mmFilm => "FocalLengthIn35mmFilm", ExifTag::SceneCaptureType => "SceneCaptureType", ExifTag::GainControl => "GainControl", ExifTag::Contrast => "Contrast", ExifTag::Saturation => "Saturation", ExifTag::Sharpness => "Sharpness", ExifTag::DeviceSettingDescription => "DeviceSettingDescription", ExifTag::SubjectDistanceRange => "SubjectDistanceRange", ExifTag::ImageUniqueID => "ImageUniqueID", ExifTag::LensSpecification => "LensSpecification", ExifTag::LensMake => "LensMake", ExifTag::LensModel => "LensModel", ExifTag::Gamma => "Gamma", ExifTag::GPSTimeStamp => "GPSTimeStamp", ExifTag::GPSSatellites => "GPSSatellites", ExifTag::GPSStatus => "GPSStatus", ExifTag::GPSMeasureMode => "GPSMeasureMode", ExifTag::GPSDOP => "GPSDOP", ExifTag::GPSSpeedRef => "GPSSpeedRef", ExifTag::GPSSpeed => "GPSSpeed", ExifTag::GPSTrackRef => "GPSTrackRef", ExifTag::GPSTrack => "GPSTrack", ExifTag::GPSImgDirectionRef => "GPSImgDirectionRef", ExifTag::GPSImgDirection => "GPSImgDirection", ExifTag::GPSMapDatum => "GPSMapDatum", ExifTag::GPSDestLatitudeRef => "GPSDestLatitudeRef", ExifTag::GPSDestLatitude => "GPSDestLatitude", ExifTag::GPSDestLongitudeRef => "GPSDestLongitudeRef", ExifTag::GPSDestLongitude => "GPSDestLongitude", ExifTag::GPSDestBearingRef => "GPSDestBearingRef", ExifTag::GPSDestBearing => "GPSDestBearing", ExifTag::GPSDestDistanceRef => "GPSDestDistanceRef", ExifTag::GPSDestDistance => "GPSDestDistance", ExifTag::GPSProcessingMethod => "GPSProcessingMethod", ExifTag::GPSAreaInformation => "GPSAreaInformation", ExifTag::GPSDateStamp => "GPSDateStamp", ExifTag::GPSDifferential => "GPSDifferential", ExifTag::YCbCrPositioning => "YCbCrPositioning", ExifTag::RecommendedExposureIndex => "RecommendedExposureIndex", ExifTag::SubSecTimeDigitized => "SubSecTimeDigitized", ExifTag::SubSecTimeOriginal => "SubSecTimeOriginal", ExifTag::SubSecTime => "SubSecTime", ExifTag::InteropOffset => "InteropOffset", ExifTag::ComponentsConfiguration => "ComponentsConfiguration", ExifTag::ThumbnailOffset => "ThumbnailOffset", ExifTag::ThumbnailLength => "ThumbnailLength", ExifTag::Compression => "Compression", ExifTag::BitsPerSample => "BitsPerSample", ExifTag::PhotometricInterpretation => "PhotometricInterpretation", ExifTag::SamplesPerPixel => "SamplesPerPixel", ExifTag::RowsPerStrip => "RowsPerStrip", ExifTag::PlanarConfiguration => "PlanarConfiguration", } } } impl TryFrom for ExifTag { type Error = crate::Error; fn try_from(v: u16) -> Result { use ExifTag::*; let tag = match v { x if x == Make.code() => Self::Make, x if x == Model.code() => Self::Model, x if x == Orientation.code() => Self::Orientation, x if x == ImageWidth.code() => Self::ImageWidth, x if x == ImageHeight.code() => Self::ImageHeight, x if x == ISOSpeedRatings.code() => Self::ISOSpeedRatings, x if x == ShutterSpeedValue.code() => Self::ShutterSpeedValue, x if x == ExposureTime.code() => Self::ExposureTime, x if x == FNumber.code() => Self::FNumber, x if x == ExifImageWidth.code() => Self::ExifImageWidth, x if x == ExifImageHeight.code() => Self::ExifImageHeight, x if x == DateTimeOriginal.code() => Self::DateTimeOriginal, x if x == CreateDate.code() => Self::CreateDate, x if x == ModifyDate.code() => Self::ModifyDate, x if x == OffsetTime.code() => Self::OffsetTime, x if x == OffsetTimeOriginal.code() => Self::OffsetTimeOriginal, x if x == OffsetTimeDigitized.code() => Self::OffsetTimeDigitized, x if x == GPSLatitudeRef.code() => Self::GPSLatitudeRef, x if x == GPSLatitude.code() => Self::GPSLatitude, x if x == GPSLongitudeRef.code() => Self::GPSLongitudeRef, x if x == GPSLongitude.code() => Self::GPSLongitude, x if x == GPSAltitudeRef.code() => Self::GPSAltitudeRef, x if x == GPSAltitude.code() => Self::GPSAltitude, x if x == GPSVersionID.code() => Self::GPSVersionID, x if x == ExifOffset.code() => Self::ExifOffset, x if x == GPSInfo.code() => Self::GPSInfo, x if x == ImageDescription.code() => Self::ImageDescription, x if x == XResolution.code() => Self::XResolution, x if x == YResolution.code() => Self::YResolution, x if x == ResolutionUnit.code() => Self::ResolutionUnit, x if x == Software.code() => Self::Software, x if x == HostComputer.code() => Self::HostComputer, x if x == WhitePoint.code() => Self::WhitePoint, x if x == PrimaryChromaticities.code() => Self::PrimaryChromaticities, x if x == YCbCrCoefficients.code() => Self::YCbCrCoefficients, x if x == ReferenceBlackWhite.code() => Self::ReferenceBlackWhite, x if x == Copyright.code() => Self::Copyright, x if x == ExposureProgram.code() => Self::ExposureProgram, x if x == SpectralSensitivity.code() => Self::SpectralSensitivity, x if x == OECF.code() => Self::OECF, x if x == SensitivityType.code() => Self::SensitivityType, x if x == ExifVersion.code() => Self::ExifVersion, x if x == ApertureValue.code() => Self::ApertureValue, x if x == BrightnessValue.code() => Self::BrightnessValue, x if x == ExposureBiasValue.code() => Self::ExposureBiasValue, x if x == MaxApertureValue.code() => Self::MaxApertureValue, x if x == SubjectDistance.code() => Self::SubjectDistance, x if x == MeteringMode.code() => Self::MeteringMode, x if x == LightSource.code() => Self::LightSource, x if x == Flash.code() => Self::Flash, x if x == FocalLength.code() => Self::FocalLength, x if x == SubjectArea.code() => Self::SubjectArea, x if x == MakerNote.code() => Self::MakerNote, x if x == UserComment.code() => Self::UserComment, x if x == FlashPixVersion.code() => Self::FlashPixVersion, x if x == ColorSpace.code() => Self::ColorSpace, x if x == RelatedSoundFile.code() => Self::RelatedSoundFile, x if x == FlashEnergy.code() => Self::FlashEnergy, x if x == FocalPlaneXResolution.code() => Self::FocalPlaneXResolution, x if x == FocalPlaneYResolution.code() => Self::FocalPlaneYResolution, x if x == FocalPlaneResolutionUnit.code() => Self::FocalPlaneResolutionUnit, x if x == SubjectLocation.code() => Self::SubjectLocation, x if x == ExposureIndex.code() => Self::ExposureIndex, x if x == SensingMethod.code() => Self::SensingMethod, x if x == FileSource.code() => Self::FileSource, x if x == SceneType.code() => Self::SceneType, x if x == CFAPattern.code() => Self::CFAPattern, x if x == CustomRendered.code() => Self::CustomRendered, x if x == ExposureMode.code() => Self::ExposureMode, x if x == WhiteBalanceMode.code() => Self::WhiteBalanceMode, x if x == DigitalZoomRatio.code() => Self::DigitalZoomRatio, x if x == FocalLengthIn35mmFilm.code() => Self::FocalLengthIn35mmFilm, x if x == SceneCaptureType.code() => Self::SceneCaptureType, x if x == GainControl.code() => Self::GainControl, x if x == Contrast.code() => Self::Contrast, x if x == Saturation.code() => Self::Saturation, x if x == Sharpness.code() => Self::Sharpness, x if x == DeviceSettingDescription.code() => Self::DeviceSettingDescription, x if x == SubjectDistanceRange.code() => Self::SubjectDistanceRange, x if x == ImageUniqueID.code() => Self::ImageUniqueID, x if x == LensSpecification.code() => Self::LensSpecification, x if x == LensMake.code() => Self::LensMake, x if x == LensModel.code() => Self::LensModel, x if x == Gamma.code() => Self::Gamma, x if x == GPSTimeStamp.code() => Self::GPSTimeStamp, x if x == GPSSatellites.code() => Self::GPSSatellites, x if x == GPSStatus.code() => Self::GPSStatus, x if x == GPSMeasureMode.code() => Self::GPSMeasureMode, x if x == GPSDOP.code() => Self::GPSDOP, x if x == GPSSpeedRef.code() => Self::GPSSpeedRef, x if x == GPSSpeed.code() => Self::GPSSpeed, x if x == GPSTrackRef.code() => Self::GPSTrackRef, x if x == GPSTrack.code() => Self::GPSTrack, x if x == GPSImgDirectionRef.code() => Self::GPSImgDirectionRef, x if x == GPSImgDirection.code() => Self::GPSImgDirection, x if x == GPSMapDatum.code() => Self::GPSMapDatum, x if x == GPSDestLatitudeRef.code() => Self::GPSDestLatitudeRef, x if x == GPSDestLatitude.code() => Self::GPSDestLatitude, x if x == GPSDestLongitudeRef.code() => Self::GPSDestLongitudeRef, x if x == GPSDestLongitude.code() => Self::GPSDestLongitude, x if x == GPSDestBearingRef.code() => Self::GPSDestBearingRef, x if x == GPSDestBearing.code() => Self::GPSDestBearing, x if x == GPSDestDistanceRef.code() => Self::GPSDestDistanceRef, x if x == GPSDestDistance.code() => Self::GPSDestDistance, x if x == GPSProcessingMethod.code() => Self::GPSProcessingMethod, x if x == GPSAreaInformation.code() => Self::GPSAreaInformation, x if x == GPSDateStamp.code() => Self::GPSDateStamp, x if x == GPSDifferential.code() => Self::GPSDifferential, x if x == YCbCrPositioning.code() => Self::YCbCrPositioning, x if x == RecommendedExposureIndex.code() => Self::RecommendedExposureIndex, x if x == SubSecTimeDigitized.code() => Self::SubSecTimeDigitized, x if x == SubSecTimeOriginal.code() => Self::SubSecTimeOriginal, x if x == SubSecTime.code() => Self::SubSecTime, x if x == InteropOffset.code() => Self::InteropOffset, x if x == ComponentsConfiguration.code() => Self::ComponentsConfiguration, x if x == ThumbnailOffset.code() => Self::ThumbnailOffset, x if x == ThumbnailLength.code() => Self::ThumbnailLength, x if x == Compression.code() => Self::Compression, x if x == BitsPerSample.code() => Self::BitsPerSample, x if x == PhotometricInterpretation.code() => Self::PhotometricInterpretation, x if x == SamplesPerPixel.code() => Self::SamplesPerPixel, x if x == RowsPerStrip.code() => Self::RowsPerStrip, x if x == PlanarConfiguration.code() => Self::PlanarConfiguration, o => return Err(format!("Unrecognized ExifTag 0x{o:04x}").into()), }; Ok(tag) } } #[allow(unused)] pub enum Orientation { Horizontal, MirrorHorizontal, Rotate, MirrorVertical, MirrorHorizontalRotate270, Rotate90, MirrorHorizontalRotate90, Rotate270, } nom-exif-2.5.4/src/exif/travel.rs000064400000000000000000000142221046102023000147440ustar 00000000000000use nom::{ number::{streaming, Endianness}, sequence::tuple, IResult, Needed, }; use crate::{ error::ParsingError, exif::{tags::ExifTagCode, TiffHeader}, values::{array_to_string, DataFormat}, }; use super::{exif_exif::IFD_ENTRY_SIZE, exif_iter::SUBIFD_TAGS}; /// Only iterates headers, don't parse entries. /// /// Currently only used to extract Exif data for *.tiff files pub(crate) struct IfdHeaderTravel<'a> { // starts from file beginning data: &'a [u8], tag: ExifTagCode, endian: Endianness, // ifd data offset offset: usize, } #[derive(Debug, Clone)] pub(crate) struct EntryInfo<'a> { pub tag: u16, #[allow(unused)] pub data: &'a [u8], #[allow(unused)] pub data_format: DataFormat, #[allow(unused)] pub data_offset: Option, pub sub_ifd_offset: Option, } impl<'a> IfdHeaderTravel<'a> { pub fn new(input: &'a [u8], offset: usize, tag: ExifTagCode, endian: Endianness) -> Self { Self { data: input, tag, endian, offset, } } #[tracing::instrument(skip_all)] fn parse_tag_entry_header( &'a self, entry_data: &'a [u8], ) -> IResult<&'a [u8], Option>> { let endian = self.endian; let (remain, (tag, data_format, components_num, value_or_offset)) = tuple(( streaming::u16::<_, nom::error::Error<_>>(endian), streaming::u16(endian), streaming::u32(endian), streaming::u32(endian), ))(entry_data)?; if tag == 0 { return Ok((remain, None)); } let data_format: DataFormat = match data_format.try_into() { Ok(df) => df, // Ignore errors here Err(e) => { tracing::warn!(?e, "Ignored: IFD entry data format error"); return Ok((&[][..], None)); } }; // get component_size according to data format let component_size = data_format.component_size(); // get entry data let size = components_num as usize * component_size; let (data, data_offset) = if size > 4 { let start = self.get_data_pos(value_or_offset) as usize; let end = start + size; tracing::debug!( components_num, size, "tag {:04x} entry data start {:08x} end {:08x} my_offset: {:08x} data len {:08x}", tag, value_or_offset, start, end, self.data.len(), ); if end > self.data.len() { return Err(nom::Err::Incomplete(Needed::new(end - self.data.len()))); } (&self.data[start..end], Some(start as u32)) } else { (entry_data, None) }; let sub_ifd_offset = if SUBIFD_TAGS.contains(&tag) { let offset = self.get_data_pos(value_or_offset); if offset > 0 { Some(offset) } else { None } } else { None }; let entry = EntryInfo { tag, data, data_format, data_offset, sub_ifd_offset, }; Ok((&[][..], Some(entry))) } fn get_data_pos(&'a self, value_or_offset: u32) -> u32 { // value_or_offset.saturating_sub(self.offset) value_or_offset } #[tracing::instrument(skip(self))] fn parse_ifd_entry_header(&self, pos: u32) -> IResult<&[u8], Option>> { let (_, entry_data) = nom::bytes::streaming::take(IFD_ENTRY_SIZE)(&self.data[pos as usize..])?; let (remain, entry) = self.parse_tag_entry_header(entry_data)?; if let Some(entry) = entry { // if !cb(&entry) { // return Ok((&[][..], ())); // } if let Some(offset) = entry.sub_ifd_offset { let tag: ExifTagCode = entry.tag.into(); tracing::debug!(?offset, data_len = self.data.len(), "sub-ifd: {:?}", tag); // Full fill bytes until sub-ifd header let (_, _) = nom::bytes::streaming::take(offset as usize - remain.len() + 2)(self.data)?; let sub_ifd = IfdHeaderTravel::new(self.data, offset as usize, tag, self.endian); return Ok((remain, Some(sub_ifd))); } } Ok((remain, None)) } #[tracing::instrument(skip(self))] pub fn travel_ifd(&mut self, depth: usize) -> Result<(), ParsingError> { if depth >= 3 { let msg = "depth shouldn't be greater than 3"; tracing::error!(msg); return Err(ParsingError::Failed(msg.into())); } if self.offset + 2 > self.data.len() { return Err(ParsingError::Failed(format!( "invalid ifd offset: {}", self.offset ))); } let (_, entry_num) = TiffHeader::parse_ifd_entry_num(&self.data[self.offset..], self.endian)?; let mut pos = self.offset + 2; let mut sub_ifds = Vec::new(); // parse entries for _ in 0..entry_num { if pos >= self.data.len() { break; } let (_, sub_ifd) = self.parse_ifd_entry_header(pos as u32)?; pos += IFD_ENTRY_SIZE; if let Some(ifd) = sub_ifd { tracing::debug!( data = array_to_string("bytes", self.data), tag = ifd.tag.to_string(), ); sub_ifds.push(ifd); } } for mut ifd in sub_ifds { ifd.travel_ifd(depth + 1)?; } // Currently, we ignore ifd1 data in *.tif files Ok(()) } } // fn keep_incomplete_err_only(e: nom::Err) -> nom::Err { // match e { // nom::Err::Incomplete(n) => nom::Err::Incomplete(n), // nom::Err::Error(e) => nom::Err::Error(format!("parse ifd error: {:?}", e)), // nom::Err::Failure(_) => nom::Err::Failure("parse ifd failure".to_string()), // } // } nom-exif-2.5.4/src/exif.rs000064400000000000000000000310151046102023000134460ustar 00000000000000use crate::error::{nom_error_to_parsing_error_with_state, ParsingError, ParsingErrorState}; use crate::file::MimeImage; use crate::parser::{BufParser, ParsingState, ShareBuf}; use crate::raf::RafInfo; use crate::skip::Skip; use crate::slice::SubsliceRange; use crate::{heif, jpeg, MediaParser, MediaSource}; #[allow(deprecated)] use crate::{partial_vec::PartialVec, FileFormat}; pub use exif_exif::Exif; use exif_exif::{check_exif_header2, TIFF_HEADER_LEN}; use exif_iter::input_into_iter; pub use exif_iter::{ExifIter, ParsedExifEntry}; pub use gps::{GPSInfo, LatLng}; pub use tags::ExifTag; use std::io::Read; use std::ops::Range; pub(crate) mod ifd; pub(crate) use exif_exif::{check_exif_header, TiffHeader}; pub(crate) use travel::IfdHeaderTravel; mod exif_exif; mod exif_iter; mod gps; mod tags; mod travel; /// *Deprecated*: Please use [`crate::MediaParser`] instead. /// /// Read exif data from `reader`, and build an [`ExifIter`] for it. /// /// ~~If `format` is None, the parser will detect the file format automatically.~~ /// *The `format` param will be ignored from v2.0.0.* /// /// Currently supported file formats are: /// /// - *.heic, *.heif, etc. /// - *.jpg, *.jpeg, etc. /// /// *.tiff/*.tif is not supported by this function, please use `MediaParser` /// instead. /// /// All entries are lazy-parsed. That is, only when you iterate over /// [`ExifIter`] will the IFD entries be parsed one by one. /// /// The one exception is the time zone entries. The parser will try to find and /// parse the time zone data first, so we can correctly parse all time /// information in subsequent iterates. /// /// Please note that the parsing routine itself provides a buffer, so the /// `reader` may not need to be wrapped with `BufRead`. /// /// Returns: /// /// - An `Ok>` if Exif data is found and parsed successfully. /// - An `Ok` if Exif data is not found. /// - An `Err` if Exif data is found but parsing failed. #[deprecated(since = "2.0.0")] #[allow(deprecated)] pub fn parse_exif(reader: T, _: Option) -> crate::Result> { let mut parser = MediaParser::new(); let iter: ExifIter = parser.parse(MediaSource::unseekable(reader)?)?; let iter = iter.to_owned(); Ok(Some(iter)) } #[tracing::instrument(skip(reader))] pub(crate) fn parse_exif_iter>( parser: &mut MediaParser, mime_img: MimeImage, reader: &mut R, ) -> Result { let out = parser.load_and_parse::(reader, |buf, state| { extract_exif_range(mime_img, buf, state) })?; range_to_iter(parser, out) } type ExifRangeResult = Result, Option)>, ParsingErrorState>; fn extract_exif_range(img: MimeImage, buf: &[u8], state: Option) -> ExifRangeResult { let (exif_data, state) = extract_exif_with_mime(img, buf, state)?; let header = state.and_then(|x| match x { ParsingState::TiffHeader(h) => Some(h), ParsingState::HeifExifSize(_) => None, }); Ok(exif_data .and_then(|x| buf.subslice_in_range(x)) .map(|x| (x, header))) } fn range_to_iter( parser: &mut impl ShareBuf, out: Option<(Range, Option)>, ) -> Result { if let Some((range, header)) = out { tracing::debug!(?range, ?header, "Got Exif data"); let input: PartialVec = parser.share_buf(range); let iter = input_into_iter(input, header)?; Ok(iter) } else { tracing::debug!("Exif not found"); Err("Exif not found".into()) } } #[cfg(feature = "async")] #[tracing::instrument(skip(reader))] pub(crate) async fn parse_exif_iter_async< R: AsyncRead + Unpin + Send, S: crate::skip::AsyncSkip, >( parser: &mut crate::AsyncMediaParser, mime_img: MimeImage, reader: &mut R, ) -> Result { use crate::parser_async::AsyncBufParser; let out = parser .load_and_parse::(reader, |buf, state| { extract_exif_range(mime_img, buf, state) }) .await?; range_to_iter(parser, out) } #[tracing::instrument(skip(buf))] pub(crate) fn extract_exif_with_mime( img_type: crate::file::MimeImage, buf: &[u8], state: Option, ) -> Result<(Option<&[u8]>, Option), ParsingErrorState> { let (exif_data, state) = match img_type { MimeImage::Jpeg => jpeg::extract_exif_data(buf) .map(|res| (res.1, state.clone())) .map_err(|e| nom_error_to_parsing_error_with_state(e, state))?, MimeImage::Heic | crate::file::MimeImage::Heif => heif_extract_exif(state, buf)?, MimeImage::Tiff => { let header = match state { Some(ParsingState::TiffHeader(ref h)) => h.to_owned(), None => { let (_, header) = TiffHeader::parse(buf) .map_err(|e| nom_error_to_parsing_error_with_state(e, None))?; if header.ifd0_offset as usize > buf.len() { let clear_and_skip = ParsingError::Need(header.ifd0_offset as usize - TIFF_HEADER_LEN + 2); let state = Some(ParsingState::TiffHeader(header)); return Err(ParsingErrorState::new(clear_and_skip, state)); } header } _ => unreachable!(), }; // full fill TIFF data tracing::debug!("full fill TIFF data"); let mut iter = IfdHeaderTravel::new( buf, header.ifd0_offset as usize, tags::ExifTagCode::Code(0x2a), header.endian, ); iter.travel_ifd(0) .map_err(|e| ParsingErrorState::new(e, state.clone()))?; tracing::debug!("full fill TIFF data done"); (Some(buf), state) } MimeImage::Raf => RafInfo::parse(buf) .map(|res| (res.1.exif_data, state.clone())) .map_err(|e| nom_error_to_parsing_error_with_state(e, state))?, }; Ok((exif_data, state)) } fn heif_extract_exif( state: Option, buf: &[u8], ) -> Result<(Option<&[u8]>, Option), ParsingErrorState> { let (data, state) = match state { Some(ParsingState::HeifExifSize(size)) => { let (_, data) = nom::bytes::streaming::take(size)(buf) .map_err(|e| nom_error_to_parsing_error_with_state(e, state.clone()))?; (Some(data), state) } None => { let (_, meta) = heif::parse_meta_box(buf) .map_err(|e| nom_error_to_parsing_error_with_state(e, state))?; if let Some(meta) = meta { if let Some(range) = meta.exif_data_offset() { if range.end > buf.len() { let state = ParsingState::HeifExifSize(range.len()); let clear_and_skip = ParsingError::ClearAndSkip(range.start); return Err(ParsingErrorState::new(clear_and_skip, Some(state))); } else { (Some(&buf[range]), None) } } else { return Err(ParsingErrorState::new( ParsingError::Failed("no exif offset in meta box".into()), None, )); } } else { (None, None) } } _ => unreachable!(), }; let data = data.and_then(|x| check_exif_header2(x).map(|x| x.0).ok()); Ok((data, state)) } #[cfg(feature = "async")] use tokio::io::AsyncRead; /// *Deprecated*: Please use [`crate::MediaParser`] instead. /// /// `async` version of [`parse_exif`]. #[allow(deprecated)] #[cfg(feature = "async")] #[deprecated(since = "2.0.0")] pub async fn parse_exif_async( reader: T, _: Option, ) -> crate::Result> { use crate::{AsyncMediaParser, AsyncMediaSource}; let mut parser = AsyncMediaParser::new(); let exif: ExifIter = parser .parse(AsyncMediaSource::unseekable(reader).await?) .await?; Ok(Some(exif)) } #[cfg(test)] #[allow(deprecated)] mod tests { use std::{sync::mpsc, thread, time::Duration}; use crate::{ file::MimeImage, testkit::{open_sample, read_sample}, values::URational, }; use test_case::test_case; use super::*; #[test_case("exif.heic", "+43.29013+084.22713+1595.950CRSWGS_84/")] #[test_case("exif.jpg", "+22.53113+114.02148/")] #[test_case("invalid-gps", "-")] fn gps(path: &str, gps_str: &str) { let f = open_sample(path).unwrap(); let iter = parse_exif(f, None) .expect("should be Ok") .expect("should not be None"); if gps_str == "-" { assert!(iter.parse_gps_info().expect("should be ok").is_none()); } else { let gps_info = iter .parse_gps_info() .expect("should be parsed Ok") .expect("should not be None"); // let gps_info = iter // .consume_parse_gps_info() // .expect("should be parsed Ok") // .expect("should not be None"); assert_eq!(gps_info.format_iso6709(), gps_str); } } #[cfg(feature = "async")] #[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[test_case("exif.heic", "+43.29013+084.22713+1595.950CRSWGS_84/")] #[test_case("exif.jpg", "+22.53113+114.02148/")] async fn gps_async(path: &str, gps_str: &str) { use std::path::Path; use tokio::fs::File; let f = File::open(Path::new("testdata").join(path)).await.unwrap(); let iter = parse_exif_async(f, None) .await .expect("should be Ok") .expect("should not be None"); let gps_str = gps_str.to_owned(); let _ = tokio::spawn(async move { let exif: Exif = iter.into(); let gps_info = exif.get_gps_info().expect("ok").expect("some"); assert_eq!(gps_info.format_iso6709(), gps_str); }) .await; } #[test_case( "exif.jpg", 'N', [(22, 1), (31, 1), (5208, 100)].into(), 'E', [(114, 1), (1, 1), (1733, 100)].into(), 0u8, (0, 1).into(), None, None )] #[allow(clippy::too_many_arguments)] fn gps_info( path: &str, latitude_ref: char, latitude: LatLng, longitude_ref: char, longitude: LatLng, altitude_ref: u8, altitude: URational, speed_ref: Option, speed: Option, ) { let _ = tracing_subscriber::fmt().with_test_writer().try_init(); let buf = read_sample(path).unwrap(); let (data, _) = extract_exif_with_mime(MimeImage::Jpeg, &buf, None).unwrap(); let data = data.unwrap(); let subslice_in_range = buf.subslice_in_range(data).unwrap(); let iter = input_into_iter((buf, subslice_in_range), None).unwrap(); let exif: Exif = iter.into(); let gps = exif.get_gps_info().unwrap().unwrap(); assert_eq!( gps, GPSInfo { latitude_ref, latitude, longitude_ref, longitude, altitude_ref, altitude, speed_ref, speed, } ) } #[test_case("exif.heic")] fn tag_values(path: &str) { let f = open_sample(path).unwrap(); let iter = parse_exif(f, None).unwrap().unwrap(); let tags = [ExifTag::Make, ExifTag::Model]; let res: Vec = iter .clone() .filter(|e| e.tag().is_some_and(|t| tags.contains(&t))) .filter(|e| e.has_value()) .map(|e| format!("{} => {}", e.tag().unwrap(), e.get_value().unwrap())) .collect(); assert_eq!(res.join(", "), "Make => Apple, Model => iPhone 12 Pro"); } #[test] fn endless_loop() { let (sender, receiver) = mpsc::channel(); thread::spawn(move || { let name = "endless_loop.jpg"; let f = open_sample(name).unwrap(); let iter = parse_exif(f, None).unwrap().unwrap(); let _: Exif = iter.into(); sender.send(()).unwrap(); }); receiver .recv_timeout(Duration::from_secs(1)) .expect("There is an infinite loop in the parsing process!"); } } nom-exif-2.5.4/src/file.rs000064400000000000000000000277071046102023000134470ustar 00000000000000use nom::{bytes::complete, multi::many0, FindSubstring}; use std::{ fmt::Display, io::{Cursor, Read}, }; use crate::{ bbox::{travel_header, BoxHolder}, ebml::element::parse_ebml_doc_type, error::{ParsedError, ParsingError}, exif::TiffHeader, jpeg::check_jpeg, loader::Load, raf::RafInfo, slice::SubsliceRange, }; const HEIF_HEIC_BRAND_NAMES: &[&[u8]] = &[ b"heic", // the usual HEIF images b"heix", // 10bit images, or anything that uses h265 with range extension b"hevc", // 'hevx': brands for image sequences b"heim", // multiview b"heis", // scalable b"hevm", // multiview sequence b"hevs", // scalable sequence b"mif1", b"MiHE", b"miaf", b"MiHB", // HEIC file's compatible brands ]; const HEIC_BRAND_NAMES: &[&[u8]] = &[b"heic", b"heix", b"heim", b"heis"]; // TODO: Refer to the information on the website https://www.ftyps.com to add // other less common MP4 brands. const MP4_BRAND_NAMES: &[&str] = &[ "3g2a", "3g2b", "3g2c", "3ge6", "3ge7", "3gg6", "3gp4", "3gp5", "3gp6", "3gs7", "avc1", "mp41", "mp42", "iso2", "isom", "vfj1", ]; const QT_BRAND_NAMES: &[&str] = &["qt ", "mqt "]; #[derive(Debug, Clone, PartialEq, Eq, Copy)] pub(crate) enum Mime { Image(MimeImage), Video(MimeVideo), } impl Mime { pub fn unwrap_image(self) -> MimeImage { match self { Mime::Image(val) => val, Mime::Video(_) => panic!("called `Mime::unwrap_image()` on an `Mime::Video`"), } } pub fn unwrap_video(self) -> MimeVideo { match self { Mime::Image(_) => panic!("called `Mime::unwrap_video()` on an `Mime::Image`"), Mime::Video(val) => val, } } } #[derive(Debug, Clone, PartialEq, Eq, Copy)] pub(crate) enum MimeImage { Jpeg, Heic, Heif, Tiff, Raf, // Fujifilm RAW, image/x-fuji-raf } #[derive(Debug, Clone, PartialEq, Eq, Copy)] pub(crate) enum MimeVideo { QuickTime, Mp4, Webm, Matroska, _3gpp, } impl TryFrom<&[u8]> for Mime { type Error = crate::Error; fn try_from(input: &[u8]) -> Result { let mime = if let Ok(x) = parse_bmff_mime(input) { x } else if let Ok(x) = get_ebml_doc_type(input) { if x == "webm" { Mime::Video(MimeVideo::Webm) } else { Mime::Video(MimeVideo::Matroska) } } else if TiffHeader::parse(input).is_ok() { Mime::Image(MimeImage::Tiff) } else if check_jpeg(input).is_ok() { Mime::Image(MimeImage::Jpeg) } else if RafInfo::check(input).is_ok() { Mime::Image(MimeImage::Raf) } else { return Err(crate::Error::UnrecognizedFileFormat); }; Ok(mime) } } /// *Deprecated*: Please use [`MediaSource`] instead. #[deprecated(since = "2.0.0")] #[allow(unused)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FileFormat { Jpeg, /// heic, heif Heif, // Currently, there is not much difference between QuickTime and MP4 when // parsing metadata, and they share the same parsing mechanism. // // The only difference is that if detected as an MP4 file, the // `moov/udta/ยฉxyz` atom is additionally checked and an attempt is made to // read GPS information from it, since Android phones store GPS information // in that atom. /// mov QuickTime, MP4, /// webm, mkv, mka, mk3d Ebml, } // Parse the input buffer and detect its file type #[allow(deprecated)] impl TryFrom<&[u8]> for FileFormat { type Error = crate::Error; fn try_from(input: &[u8]) -> Result { if let Ok(ff) = check_bmff(input) { Ok(ff) } else if get_ebml_doc_type(input).is_ok() { Ok(Self::Ebml) } else if check_jpeg(input).is_ok() { Ok(Self::Jpeg) } else { Err(crate::Error::UnrecognizedFileFormat) } } } #[allow(deprecated)] impl FileFormat { pub fn try_from_read(reader: T) -> crate::Result { const BUF_SIZE: usize = 4096; let mut buf = Vec::with_capacity(BUF_SIZE); let n = reader.take(BUF_SIZE as u64).read_to_end(buf.as_mut())?; if n == 0 { Err("file is empty")?; } buf.as_slice().try_into() } pub(crate) fn try_from_load(loader: &mut T) -> Result { loader.load_and_parse(|x| { x.try_into() .map_err(|_| ParsingError::Failed("unrecognized file format".to_string())) }) } } #[allow(deprecated)] impl Display for FileFormat { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Jpeg => "JPEG".fmt(f), Self::Heif => "HEIF/HEIC".fmt(f), Self::QuickTime => "QuickTime".fmt(f), Self::MP4 => "MP4".fmt(f), Self::Ebml => "EBML".fmt(f), } } } fn get_ebml_doc_type(input: &[u8]) -> crate::Result { let mut cursor = Cursor::new(input); let doc = parse_ebml_doc_type(&mut cursor)?; Ok(doc) } #[tracing::instrument(skip_all)] fn parse_bmff_mime(input: &[u8]) -> crate::Result { let (ftyp, Some(major_brand)) = get_ftyp_and_major_brand(input).map_err(|_| crate::Error::UnrecognizedFileFormat)? else { if travel_header(input, |header, _| header.box_type != "mdat").is_ok() { // ftyp is None, mdat box is found, assume it's a MOV file extracted from HEIC return Ok(Mime::Video(MimeVideo::QuickTime)); } return Err(crate::Error::UnrecognizedFileFormat); }; tracing::debug!(?ftyp); // Check if it is a QuickTime file if QT_BRAND_NAMES.iter().any(|v| v.as_bytes() == major_brand) { return Ok(Mime::Video(MimeVideo::QuickTime)); } // Check if it is a HEIF file if HEIF_HEIC_BRAND_NAMES.contains(&major_brand) { if HEIC_BRAND_NAMES.contains(&major_brand) { return Ok(Mime::Image(MimeImage::Heic)); } return Ok(Mime::Image(MimeImage::Heif)); } // Check if it is a MP4 file if MP4_BRAND_NAMES.iter().any(|v| v.as_bytes() == major_brand) { if major_brand.starts_with(b"3gp") { return Ok(Mime::Video(MimeVideo::_3gpp)); } return Ok(Mime::Video(MimeVideo::Mp4)); } // Check compatible brands let compatible_brands = ftyp.body_data(); if QT_BRAND_NAMES .iter() .any(|v| compatible_brands.find_substring(v.as_bytes()).is_some()) { return Ok(Mime::Video(MimeVideo::QuickTime)); } if HEIF_HEIC_BRAND_NAMES .iter() .any(|x| compatible_brands.find_substring(*x).is_some()) { if HEIC_BRAND_NAMES.contains(&major_brand) { return Ok(Mime::Image(MimeImage::Heic)); } return Ok(Mime::Image(MimeImage::Heif)); } if MP4_BRAND_NAMES .iter() .any(|v| compatible_brands.subslice_in_range(v.as_bytes()).is_some()) { if major_brand.starts_with(b"3gp") { return Ok(Mime::Video(MimeVideo::_3gpp)); } return Ok(Mime::Video(MimeVideo::Mp4)); } tracing::warn!( marjor_brand = major_brand.iter().map(|b| *b as char).collect::(), "unknown major brand", ); if travel_header(input, |header, _| header.box_type != "mdat").is_ok() { // mdat box found, assume it's a mp4 file return Ok(Mime::Video(MimeVideo::Mp4)); } Err(crate::Error::UnrecognizedFileFormat) } #[allow(deprecated)] fn check_bmff(input: &[u8]) -> crate::Result { let (ftyp, Some(major_brand)) = get_ftyp_and_major_brand(input)? else { if travel_header(input, |header, _| header.box_type != "mdat").is_ok() { // ftyp is None, mdat box is found, assume it's a MOV file extracted from HEIC return Ok(FileFormat::QuickTime); } return Err(crate::Error::UnrecognizedFileFormat); }; // Check if it is a QuickTime file if QT_BRAND_NAMES.iter().any(|v| v.as_bytes() == major_brand) { return Ok(FileFormat::QuickTime); } // Check if it is a HEIF file if HEIF_HEIC_BRAND_NAMES.contains(&major_brand) { return Ok(FileFormat::Heif); } // Check if it is a MP4 file if MP4_BRAND_NAMES.iter().any(|v| v.as_bytes() == major_brand) { return Ok(FileFormat::MP4); } // Check compatible brands let compatible_brands = get_compatible_brands(ftyp.body_data())?; if QT_BRAND_NAMES .iter() .any(|v| compatible_brands.iter().any(|x| v.as_bytes() == *x)) { return Ok(FileFormat::QuickTime); } if HEIF_HEIC_BRAND_NAMES .iter() .any(|x| compatible_brands.contains(x)) { return Ok(FileFormat::Heif); } if MP4_BRAND_NAMES .iter() .any(|v| compatible_brands.iter().any(|x| v.as_bytes() == *x)) { return Ok(FileFormat::MP4); } tracing::warn!( marjor_brand = major_brand.iter().map(|b| *b as char).collect::(), "unknown major brand", ); if travel_header(input, |header, _| header.box_type != "mdat").is_ok() { // find mdat box, assume it's a mp4 file return Ok(FileFormat::MP4); } Err(crate::Error::UnrecognizedFileFormat) } fn get_ftyp_and_major_brand(input: &[u8]) -> crate::Result<(BoxHolder, Option<&[u8]>)> { let (_, bbox) = BoxHolder::parse(input).map_err(|e| format!("parse ftyp failed: {e}"))?; if bbox.box_type() == "ftyp" { if bbox.body_data().len() < 4 { return Err(format!( "parse ftyp failed; body size should greater than 4, got {}", bbox.body_data().len() ) .into()); } let (_, ftyp) = complete::take(4_usize)(bbox.body_data())?; Ok((bbox, Some(ftyp))) } else if bbox.box_type() == "wide" { // MOV files that extracted from HEIC starts with `wide` & `mdat` atoms Ok((bbox, None)) } else { Err(format!("parse ftyp failed; first box type is: {}", bbox.box_type()).into()) } } fn get_compatible_brands(body: &[u8]) -> crate::Result> { let Ok((_, brands)) = many0(complete::take::>( 4_usize, ))(body) else { return Err("get compatible brands failed".into()); }; Ok(brands) } #[allow(deprecated)] #[cfg(test)] mod tests { use std::ops::Deref; use super::*; use test_case::test_case; use Mime::*; use MimeImage::*; use MimeVideo::*; use crate::testkit::{open_sample, read_sample}; #[test_case("exif.heic", Image(Heic))] #[test_case("exif.jpg", Image(Jpeg))] #[test_case("fujifilm_x_t1_01.raf.meta", Image(Raf))] #[test_case("meta.mp4", Video(Mp4))] #[test_case("meta.mov", Video(QuickTime))] #[test_case("embedded-in-heic.mov", Video(QuickTime))] #[test_case("compatible-brands.mov", Video(QuickTime))] #[test_case("webm_480.webm", Video(Webm))] #[test_case("mkv_640x360.mkv", Video(Matroska))] #[test_case("mka.mka", Video(Matroska))] #[test_case("3gp_640x360.3gp", Video(_3gpp))] fn mime(path: &str, mime: Mime) { let data = read_sample(path).unwrap(); let m: Mime = data.deref().try_into().unwrap(); assert_eq!(m, mime); } #[test_case("exif.heic", FileFormat::Heif)] #[test_case("exif.jpg", FileFormat::Jpeg)] #[test_case("meta.mov", FileFormat::QuickTime)] #[test_case("meta.mp4", FileFormat::MP4)] #[test_case("embedded-in-heic.mov", FileFormat::QuickTime)] #[test_case("compatible-brands.mov", FileFormat::QuickTime)] fn file_format(path: &str, expect: FileFormat) { let f = open_sample(path).unwrap(); let ff = FileFormat::try_from_read(f).unwrap(); assert_eq!(ff, expect); } #[test_case("compatible-brands-fail.mov")] fn file_format_error(path: &str) { let f = open_sample(path).unwrap(); FileFormat::try_from_read(f).unwrap_err(); } } nom-exif-2.5.4/src/heif.rs000064400000000000000000000102201046102023000134210ustar 00000000000000use std::io::{Read, Seek}; use nom::combinator::fail; use nom::{number::complete::be_u32, IResult}; use crate::bbox::find_box; use crate::exif::Exif; use crate::{ bbox::{BoxHolder, MetaBox, ParseBox}, exif::check_exif_header, }; use crate::{ExifIter, MediaParser, MediaSource}; /// *Deprecated*: Please use [`MediaParser`] + [`MediaSource`] instead. /// /// Analyze the byte stream in the `reader` as a HEIF/HEIC file, attempting to /// extract Exif data it may contain. /// /// Please note that the parsing routine itself provides a buffer, so the /// `reader` may not need to be wrapped with `BufRead`. /// /// # Usage /// /// ```rust /// use nom_exif::*; /// use nom_exif::ExifTag::*; /// /// use std::fs::File; /// use std::path::Path; /// /// let f = File::open(Path::new("./testdata/exif.heic")).unwrap(); /// let exif = parse_heif_exif(f).unwrap().unwrap(); /// /// assert_eq!(exif.get(Make).unwrap().to_string(), "Apple"); /// ``` /// /// See also: [`parse_exif`](crate::parse_exif). #[deprecated(since = "2.0.0")] pub fn parse_heif_exif(reader: R) -> crate::Result> { let parser = &mut MediaParser::new(); let iter: ExifIter = parser.parse(MediaSource::seekable(reader)?)?; Ok(Some(iter.into())) } /// Extract Exif TIFF data from the bytes of a HEIF/HEIC file. #[allow(unused)] #[tracing::instrument(skip_all)] pub(crate) fn extract_exif_data(input: &[u8]) -> IResult<&[u8], Option<&[u8]>> { let (remain, meta) = parse_meta_box(input)?; if let Some(meta) = meta { extract_exif_with_meta(input, &meta) } else { Ok((remain, None)) } } pub(crate) fn parse_meta_box(input: &[u8]) -> IResult<&[u8], Option> { let remain = input; let (remain, bbox) = BoxHolder::parse(remain)?; if bbox.box_type() != "ftyp" { return fail(input); } let (remain, Some(bbox)) = find_box(remain, "meta")? else { tracing::debug!(?bbox, "meta box not found"); return Ok((remain, None)); }; tracing::debug!( ?bbox, pos = input.len() - remain.len() - bbox.header.box_size as usize, "Got meta box" ); let (_, bbox) = MetaBox::parse_box(bbox.data)?; tracing::debug!(?bbox, "meta box parsed"); Ok((remain, Some(bbox))) } pub(crate) fn extract_exif_with_meta<'a>( input: &'a [u8], bbox: &MetaBox, ) -> IResult<&'a [u8], Option<&'a [u8]>> { let (out_remain, data) = bbox.exif_data(input)?; tracing::debug!( data_len = data.as_ref().map(|x| x.len()), "exif data extracted" ); if let Some(data) = data { let (remain, _) = be_u32(data)?; if check_exif_header(remain)? { Ok((out_remain, Some(&remain[6..]))) // Safe-slice } else { Ok((out_remain, None)) } } else { Ok((out_remain, None)) } } #[allow(deprecated)] #[cfg(test)] mod tests { use super::*; use crate::testkit::*; use test_case::test_case; #[test_case("exif.heic")] fn heif(path: &str) { let _ = tracing_subscriber::fmt().with_test_writer().try_init(); let reader = open_sample(path).unwrap(); let exif = parse_heif_exif(reader).unwrap().unwrap(); let mut expect = String::new(); open_sample(&format!("{path}.sorted.txt")) .unwrap() .read_to_string(&mut expect) .unwrap(); assert_eq!(sorted_exif_entries(&exif).join("\n"), expect.trim()); } #[test_case("ramdisk.img")] fn invalid_heic(path: &str) { let _ = tracing_subscriber::fmt().with_test_writer().try_init(); let reader = open_sample(path).unwrap(); parse_heif_exif(reader).expect_err("should be ParseFailed error"); } #[test_case("exif-one-entry.heic", 0x24-10)] #[test_case("exif.heic", 0xa3a-10)] fn heic_exif_data(path: &str, exif_size: usize) { let _ = tracing_subscriber::fmt().with_test_writer().try_init(); let buf = read_sample(path).unwrap(); let (_, exif) = extract_exif_data(&buf[..]).unwrap(); if exif_size == 0 { assert!(exif.is_none()); } else { assert_eq!(exif.unwrap().len(), exif_size); } } } nom-exif-2.5.4/src/jpeg.rs000064400000000000000000000237111046102023000134440ustar 00000000000000use crate::{ExifIter, MediaParser, MediaSource}; use std::io::{Read, Seek}; use nom::{bytes::streaming, combinator::fail, number, sequence::tuple, IResult}; use crate::exif::{check_exif_header, Exif}; /// *Deprecated*: Please use [`MediaParser`] + [`MediaSource`] instead. /// /// Analyze the byte stream in the `reader` as a JPEG file, attempting to /// extract Exif data it may contain. /// /// Please note that the parsing routine itself provides a buffer, so the /// `reader` may not need to be wrapped with `BufRead`. /// /// # Usage /// /// ```rust /// use nom_exif::*; /// use nom_exif::ExifTag::*; /// /// use std::fs::File; /// use std::path::Path; /// /// let f = File::open(Path::new("./testdata/exif.jpg")).unwrap(); /// let exif = parse_jpeg_exif(f).unwrap().unwrap(); /// /// assert_eq!(exif.get_value(&Make).unwrap().unwrap().to_string(), "vivo"); /// /// assert_eq!( /// exif.get_values(&[DateTimeOriginal, CreateDate, ModifyDate]) /// .into_iter() /// .map(|x| (x.0.to_string(), x.1.to_string())) /// .collect::>(), /// [ /// ("DateTimeOriginal", "2023-07-09T20:36:33+08:00"), /// ("CreateDate", "2023-07-09T20:36:33+08:00"), /// ("ModifyDate", "2023-07-09T20:36:33+08:00") /// ] /// .into_iter() /// .map(|x| (x.0.to_string(), x.1.to_string())) /// .collect::>() /// ); /// ``` #[deprecated(since = "2.0.0")] pub fn parse_jpeg_exif(reader: R) -> crate::Result> { let mut parser = MediaParser::new(); let iter: ExifIter = parser.parse(MediaSource::unseekable(reader)?)?; Ok(Some(iter.into())) } /// Extract Exif TIFF data from the bytes of a JPEG file. pub(crate) fn extract_exif_data(input: &[u8]) -> IResult<&[u8], Option<&[u8]>> { let (remain, segment) = find_exif_segment(input)?; let data = segment.and_then(|segment| { if segment.payload_len() <= 6 { None } else { Some(&segment.payload[6..]) // Safe-slice } }); Ok((remain, data)) } struct Segment<'a> { marker_code: u8, payload: &'a [u8], } impl Segment<'_> { pub fn payload_len(&self) -> usize { self.payload.len() } } fn find_exif_segment(input: &[u8]) -> IResult<&[u8], Option>> { let mut remain = input; let (remain, segment) = loop { let (rem, (_, code)) = tuple((streaming::tag([0xFF]), number::streaming::u8))(remain)?; let (rem, segment) = parse_segment(code, rem)?; // Sanity check assert!(rem.len() < remain.len()); remain = rem; tracing::debug!( marker = format!("0x{:04x}", segment.marker_code), size = format!("0x{:04x}", segment.payload.len()), "got segment" ); let s = &segment; if (s.marker_code == MarkerCode::APP1.code() && check_exif_header(s.payload)?) || s.marker_code == MarkerCode::Sos.code() // searching stop at SOS { break (remain, segment); } }; if segment.marker_code != MarkerCode::Sos.code() { Ok((remain, Some(segment))) } else { Ok((remain, None)) } } pub fn check_jpeg(input: &[u8]) -> crate::Result<()> { // check soi marker [0xff, 0xd8] let (_, (_, code)) = tuple((nom::bytes::complete::tag([0xFF]), number::complete::u8))(input)?; // SOI has no payload if code != MarkerCode::Soi.code() { return Err("invalid JPEG file; SOI marker not found".into()); } // check next marker [0xff, *] let (_, (_, _)) = tuple((nom::bytes::complete::tag([0xFF]), number::complete::u8))(input)?; Ok(()) } fn parse_segment(marker_code: u8, input: &[u8]) -> IResult<&[u8], Segment<'_>> { let remain = input; // SOI has no payload if marker_code == MarkerCode::Soi.code() { Ok(( remain, Segment { marker_code, payload: b"", }, )) } else { let (remain, size) = number::streaming::be_u16(remain)?; if size < 2 { return fail(remain); } // size contains the two bytes of `size` itself let (remain, data) = streaming::take(size - 2)(remain)?; Ok(( remain, Segment { marker_code, payload: data, }, )) } } /// Read all image data after the first SOS marker & before EOI marker. /// /// The returned data might include several other SOS markers if the image is a /// progressive JPEG. #[allow(dead_code)] fn read_image_data(mut reader: T) -> crate::Result> { let mut header = [0u8; 2]; loop { reader.read_exact(&mut header)?; let (tag, marker) = (header[0], header[1]); if tag != 0xFF { return Err("".into()); } if marker == MarkerCode::Soi.code() { // SOI has no body continue; } if marker == MarkerCode::Eoi.code() { return Err("exif not found".into()); } if marker == MarkerCode::Sos.code() { // found it let mut data = Vec::new(); reader.read_to_end(&mut data)?; // remove tail data loop { let Some(tail) = data.pop() else { // empty break; }; if tail == MarkerCode::Eoi.code() { if let Some(tail) = data.pop() { if tail == 0xFF { // EOI marker has been popped break; } } } } return Ok(data); } else { // skip other markers reader.read_exact(&mut header)?; let len = u16::from_be_bytes([header[0], header[1]]); reader.seek(std::io::SeekFrom::Current(len as i64 - 2))?; } } } /// A marker code is a byte following 0xFF that indicates the kind of marker. enum MarkerCode { // Start of Image Soi = 0xD8, // APP1 marker APP1 = 0xE1, // Start of Scan Sos = 0xDA, // End of Image Eoi = 0xD9, } impl MarkerCode { fn code(self) -> u8 { self as u8 } } #[cfg(test)] mod tests { use super::*; use crate::exif::ExifTag::*; use crate::testkit::*; use test_case::test_case; #[test_case("exif.jpg", true)] #[test_case("broken.jpg", true)] #[test_case("no-exif.jpg", false)] fn test_check_jpeg(path: &str, has_exif: bool) { let data = read_sample(path).unwrap(); check_jpeg(&data).unwrap(); let (_, data) = extract_exif_data(&data).unwrap(); if has_exif { data.unwrap(); } } #[test_case("exif.jpg")] #[allow(deprecated)] fn jpeg(path: &str) { let _ = tracing_subscriber::fmt().with_test_writer().try_init(); let f = open_sample(path).unwrap(); let exif = parse_jpeg_exif(f).unwrap().unwrap(); // TODO // assert_eq!( // sorted_exif_entries(&exif).join("\n"), // ); assert_eq!(exif.get_value(&Make).unwrap().unwrap().to_string(), "vivo"); assert_eq!( exif.get_values(&[DateTimeOriginal, CreateDate, ModifyDate]) .into_iter() .map(|x| (x.0.to_string(), x.1.to_string())) .collect::>(), [ ("DateTimeOriginal", "2023-07-09T20:36:33+08:00"), ("CreateDate", "2023-07-09T20:36:33+08:00"), ("ModifyDate", "2023-07-09T20:36:33+08:00") ] .into_iter() .map(|x| (x.0.to_string(), x.1.to_string())) .collect::>() ); let mut entries = exif .get_values(&[ImageWidth, ImageHeight]) .into_iter() .map(|x| (x.0.to_string(), x.1.to_string())) .collect::>(); entries.sort(); assert_eq!( entries, [("ImageHeight", "4096"), ("ImageWidth", "3072")] .into_iter() .map(|x| (x.0.to_string(), x.1.to_string())) .collect::>() ); } #[test_case("no-exif.jpg", 0)] #[test_case("exif.jpg", 0x4569-2)] fn jpeg_find_exif(path: &str, exif_size: usize) { let _ = tracing_subscriber::fmt().with_test_writer().try_init(); let buf = read_sample(path).unwrap(); let (_, segment) = find_exif_segment(&buf[..]).unwrap(); if exif_size == 0 { assert!(segment.is_none()); } else { assert_eq!(segment.unwrap().payload_len(), exif_size); } } #[test_case("no-exif.jpg", 0)] #[test_case("exif.jpg", 0x4569-8)] fn jpeg_exif_data(path: &str, exif_size: usize) { let _ = tracing_subscriber::fmt().with_test_writer().try_init(); let buf = read_sample(path).unwrap(); let (_, exif) = extract_exif_data(&buf[..]).unwrap(); if exif_size == 0 { assert!(exif.is_none()); } else { assert_eq!(exif.unwrap().len(), exif_size); } } #[test_case("no-exif.jpg", 4089704, 0x000c0301, 0xb3b3e43f)] #[test_case("exif.jpg", 3564768, 0x000c0301, 0x84a297a9)] fn jpeg_image_data(path: &str, len: usize, start: u32, end: u32) { let _ = tracing_subscriber::fmt().with_test_writer().try_init(); let f = open_sample(path).unwrap(); let data = read_image_data(f).unwrap(); assert_eq!(data.len(), len); assert_eq!(u32::from_be_bytes(data[..4].try_into().unwrap()), start); // Safe-slice in test_case assert_eq!( u32::from_be_bytes(data[data.len() - 4..].try_into().unwrap()), // Safe-slice in test_case end ); } #[allow(deprecated)] #[test] fn broken_jpg() { let _ = tracing_subscriber::fmt().with_test_writer().try_init(); let f = open_sample("broken.jpg").unwrap(); parse_jpeg_exif(f).unwrap(); } } nom-exif-2.5.4/src/lib.rs000064400000000000000000000267341046102023000132750ustar 00000000000000//! `nom-exif` is an Exif/metadata parsing library written in pure Rust with //! [nom](https://github.com/rust-bakery/nom). //! //! ## Supported File Types //! //! - Image //! - *.heic, *.heif, etc. //! - *.jpg, *.jpeg //! - *.tiff, *.tif //! - *.RAF (Fujifilm RAW) //! - Video/Audio //! - ISO base media file format (ISOBMFF): *.mp4, *.mov, *.3gp, etc. //! - Matroska based file format: *.webm, *.mkv, *.mka, etc. //! //! ## Key Features //! //! - Ergonomic Design //! //! - **Unified Workflow** for Various File Types //! //! Now, multimedia files of different types and formats (including images, //! videos, and audio) can be processed using a unified method. This consistent //! API interface simplifies user experience and reduces cognitive load. //! //! The usage is demonstrated in the following examples. `examples/rexiftool` //! is also a good example. //! //! - Two style APIs for Exif //! //! *iterator* style ([`ExifIter`]) and *get* style ([`Exif`]). The former is //! parse-on-demand, and therefore, more detailed error information can be //! captured; the latter is simpler and easier to use. //! //! - Performance //! //! - *Zero-copy* when appropriate: Use borrowing and slicing instead of //! copying whenever possible. //! //! - Minimize I/O operations: When metadata is stored at the end/middle of a //! large file (such as a QuickTime file does), `Seek` rather than `Read` //! to quickly locate the location of the metadata (if the reader supports //! `Seek`). //! //! - Share I/O and parsing buffer between multiple parse calls: This can //! improve performance and avoid the overhead and memory fragmentation //! caused by frequent memory allocation. This feature is very useful when //! you need to perform batch parsing. //! //! - Pay as you go: When working with [`ExifIter`], all entries are //! lazy-parsed. That is, only when you iterate over [`ExifIter`] will the //! IFD entries be parsed one by one. //! //! - Robustness and stability //! //! Through long-term [Fuzz testing](https://github.com/rust-fuzz/afl.rs), and //! tons of crash issues discovered during testing have been fixed. Thanks to //! [@sigaloid](https://github.com/sigaloid) for [pointing this //! out](https://github.com/mindeng/nom-exif/pull/5)! //! //! - Supports both *sync* and *async* APIs //! //! ## Unified Workflow for Various File Types //! //! By using `MediaSource` & `MediaParser`, multimedia files of different types and //! formats (including images, videos, and audio) can be processed using a unified //! method. //! //! Here's an example: //! //! ```rust //! use nom_exif::*; //! //! fn main() -> Result<()> { //! let mut parser = MediaParser::new(); //! //! let files = [ //! "./testdata/exif.heic", //! "./testdata/exif.jpg", //! "./testdata/tif.tif", //! "./testdata/meta.mov", //! "./testdata/meta.mp4", //! "./testdata/webm_480.webm", //! "./testdata/mkv_640x360.mkv", //! "./testdata/mka.mka", //! "./testdata/3gp_640x360.3gp" //! ]; //! //! for f in files { //! let ms = MediaSource::file_path(f)?; //! //! if ms.has_exif() { //! // Parse the file as an Exif-compatible file //! let mut iter: ExifIter = parser.parse(ms)?; //! // ... //! } else if ms.has_track() { //! // Parse the file as a track //! let info: TrackInfo = parser.parse(ms)?; //! // ... //! } //! } //! //! Ok(()) //! } //! ``` //! //! ## Sync API: `MediaSource` + `MediaParser` //! //! `MediaSource` is an abstraction of multimedia data sources, which can be //! created from any object that implements the `Read` trait, and can be parsed by //! `MediaParser`. //! //! Example: //! //! ```rust //! use nom_exif::*; //! //! fn main() -> Result<()> { //! let mut parser = MediaParser::new(); //! //! let ms = MediaSource::file_path("./testdata/exif.heic")?; //! assert!(ms.has_exif()); //! //! let mut iter: ExifIter = parser.parse(ms)?; //! let exif: Exif = iter.into(); //! assert_eq!(exif.get(ExifTag::Make).unwrap().as_str().unwrap(), "Apple"); //! //! let ms = MediaSource::file_path("./testdata/meta.mov")?; //! assert!(ms.has_track()); //! //! let info: TrackInfo = parser.parse(ms)?; //! assert_eq!(info.get(TrackInfoTag::Make), Some(&"Apple".into())); //! assert_eq!(info.get(TrackInfoTag::Model), Some(&"iPhone X".into())); //! assert_eq!(info.get(TrackInfoTag::GpsIso6709), Some(&"+27.1281+100.2508+000.000/".into())); //! assert_eq!(info.get_gps_info().unwrap().latitude_ref, 'N'); //! assert_eq!( //! info.get_gps_info().unwrap().latitude, //! [(27, 1), (7, 1), (68, 100)].into(), //! ); //! //! // `MediaSource` can also be created from a `TcpStream`: //! // let ms = MediaSource::tcp_stream(stream)?; //! //! // Or from any `Read + Seek`: //! // let ms = MediaSource::seekable(stream)?; //! //! // From any `Read`: //! // let ms = MediaSource::unseekable(stream)?; //! //! Ok(()) //! } //! ``` //! //! See [`MediaSource`] & [`MediaParser`] for more information. //! //! ## Async API: `AsyncMediaSource` + `AsyncMediaParser` //! //! Likewise, `AsyncMediaParser` is an abstraction for asynchronous multimedia data //! sources, which can be created from any object that implements the `AsyncRead` //! trait, and can be parsed by `AsyncMediaParser`. //! //! Enable `async` feature flag for `nom-exif` in your `Cargo.toml`: //! //! ```toml //! [dependencies] //! nom-exif = { version = "1", features = ["async"] } //! ``` //! //! See [`AsyncMediaSource`] & [`AsyncMediaParser`] for more information. //! //! ## GPS Info //! //! `ExifIter` provides a convenience method for parsing gps information. (`Exif` & //! `TrackInfo` also provide a `get_gps_info` method). //! //! ```rust //! use nom_exif::*; //! //! fn main() -> Result<()> { //! let mut parser = MediaParser::new(); //! //! let ms = MediaSource::file_path("./testdata/exif.heic")?; //! let iter: ExifIter = parser.parse(ms)?; //! //! let gps_info = iter.parse_gps_info()?.unwrap(); //! assert_eq!(gps_info.format_iso6709(), "+43.29013+084.22713+1595.950CRSWGS_84/"); //! assert_eq!(gps_info.latitude_ref, 'N'); //! assert_eq!(gps_info.longitude_ref, 'E'); //! assert_eq!( //! gps_info.latitude, //! [(43, 1), (17, 1), (2446, 100)].into(), //! ); //! Ok(()) //! } //! ``` //! //! For more usage details, please refer to the [API //! documentation](https://docs.rs/nom-exif/latest/nom_exif/). //! //! ## CLI Tool `rexiftool` //! //! ### Human Readable Output //! //! `cargo run --example rexiftool testdata/meta.mov`: //! //! ``` text //! Make => Apple //! Model => iPhone X //! Software => 12.1.2 //! CreateDate => 2024-02-02T08:09:57+00:00 //! DurationMs => 500 //! ImageWidth => 720 //! ImageHeight => 1280 //! GpsIso6709 => +27.1281+100.2508+000.000/ //! ``` //! //! ### Json Dump //! //! `cargo run --example rexiftool testdata/meta.mov -j`: //! //! ``` text //! { //! "ImageWidth": "720", //! "Software": "12.1.2", //! "ImageHeight": "1280", //! "Make": "Apple", //! "GpsIso6709": "+27.1281+100.2508+000.000/", //! "CreateDate": "2024-02-02T08:09:57+00:00", //! "Model": "iPhone X", //! "DurationMs": "500" //! } //! ``` //! //! ### Parsing Files in Directory //! //! `rexiftool` also supports batch parsing of all files in a folder //! (non-recursive). //! //! `cargo run --example rexiftool testdata/`: //! //! ```text //! File: "testdata/embedded-in-heic.mov" //! ------------------------------------------------ //! Make => Apple //! Model => iPhone 15 Pro //! Software => 17.1 //! CreateDate => 2023-11-02T12:01:02+00:00 //! DurationMs => 2795 //! ImageWidth => 1920 //! ImageHeight => 1440 //! GpsIso6709 => +22.5797+113.9380+028.396/ //! //! File: "testdata/compatible-brands-fail.heic" //! ------------------------------------------------ //! Unrecognized file format, consider filing a bug @ https://github.com/mindeng/nom-exif. //! //! File: "testdata/webm_480.webm" //! ------------------------------------------------ //! CreateDate => 2009-09-09T09:09:09+00:00 //! DurationMs => 30543 //! ImageWidth => 480 //! ImageHeight => 270 //! //! File: "testdata/mka.mka" //! ------------------------------------------------ //! DurationMs => 3422 //! ImageWidth => 0 //! ImageHeight => 0 //! //! File: "testdata/exif-one-entry.heic" //! ------------------------------------------------ //! Orientation => 1 //! //! File: "testdata/no-exif.jpg" //! ------------------------------------------------ //! Error: parse failed: Exif not found //! //! File: "testdata/exif.jpg" //! ------------------------------------------------ //! ImageWidth => 3072 //! Model => vivo X90 Pro+ //! ImageHeight => 4096 //! ModifyDate => 2023-07-09T20:36:33+08:00 //! YCbCrPositioning => 1 //! ExifOffset => 201 //! MakerNote => Undefined[0x30] //! RecommendedExposureIndex => 454 //! SensitivityType => 2 //! ISOSpeedRatings => 454 //! ExposureProgram => 2 //! FNumber => 175/100 (1.7500) //! ExposureTime => 9997/1000000 (0.0100) //! SensingMethod => 2 //! SubSecTimeDigitized => 616 //! OffsetTimeOriginal => +08:00 //! SubSecTimeOriginal => 616 //! OffsetTime => +08:00 //! SubSecTime => 616 //! FocalLength => 8670/1000 (8.6700) //! Flash => 16 //! LightSource => 21 //! MeteringMode => 1 //! SceneCaptureType => 0 //! UserComment => filter: 0; fileterIntensity: 0.0; filterMask: 0; algolist: 0; //! ... //! ``` pub use parser::{MediaParser, MediaSource}; pub use video::{TrackInfo, TrackInfoTag}; #[cfg(feature = "async")] pub use parser_async::{AsyncMediaParser, AsyncMediaSource}; pub use exif::{Exif, ExifIter, ExifTag, GPSInfo, LatLng, ParsedExifEntry}; pub use values::{EntryValue, IRational, URational}; #[allow(deprecated)] pub use exif::parse_exif; #[cfg(feature = "async")] #[allow(deprecated)] pub use exif::parse_exif_async; #[allow(deprecated)] pub use heif::parse_heif_exif; #[allow(deprecated)] pub use jpeg::parse_jpeg_exif; pub use error::Error; pub type Result = std::result::Result; pub(crate) use skip::{Seekable, Unseekable}; #[allow(deprecated)] pub use file::FileFormat; #[allow(deprecated)] pub use mov::{parse_metadata, parse_mov_metadata}; mod bbox; mod buffer; mod ebml; mod error; mod exif; mod file; mod heif; mod jpeg; mod loader; mod mov; mod parser; #[cfg(feature = "async")] mod parser_async; mod partial_vec; mod raf; mod skip; mod slice; mod utils; mod values; mod video; #[cfg(test)] mod testkit; nom-exif-2.5.4/src/loader/sync.rs000064400000000000000000000065471046102023000147510ustar 00000000000000use std::{io::Read, marker::PhantomData}; use crate::skip::Skip; use super::{BufLoad, Load, INIT_BUF_SIZE}; /// Loads bytes from `R` using an internally maintained buffer. /// /// Since Rust doesn't currently support /// [specialization](https://rust-lang.github.io/rfcs/1210-impl-specialization.html) /// , so the struct have to let user to tell it if the reader supports `Seek`, /// in the following way: /// /// - `let loader = BufLoader::::new(reader);` means the `reader` /// doesn't support `Seek`. /// /// - `let loader = BufLoader::::new(reader);` means the `reader` /// supports `Seek`. /// /// Performance impact: /// /// - If the reader supports `Seek`, the parser will use `Seek` to achieve /// efficient positioning operations in the byte stream. /// /// - Otherwise, the parser will fallback to skip certain bytes through Read. /// This may have a certain impact on performance when processing certain large /// files. For example, *.mov files place metadata at the end of the file. pub(crate) struct BufLoader { inner: Inner, } impl, R: Read> Load for BufLoader { #[inline] fn read_buf(&mut self, to_read: usize) -> std::io::Result { self.inner.read_buf(to_read) } #[inline] fn skip(&mut self, n: usize) -> std::io::Result<()> { if S::skip_by_seek(&mut self.inner.read, n as u64)? { Ok(()) } else { // S::skip(&mut self.inner.read, n as u64) self.inner.skip_by_read(n) } } } impl BufLoad for BufLoader { #[inline] fn into_vec(self) -> Vec { self.inner.buf } #[inline] fn buf(&self) -> &[u8] { &self.inner.buf } #[inline] fn buf_mut(&mut self) -> &mut Vec { &mut self.inner.buf } } impl std::ops::Index for BufLoader where Idx: std::slice::SliceIndex<[u8]>, { type Output = Idx::Output; fn index(&self, index: Idx) -> &Self::Output { &self.inner.buf[index] } } impl BufLoader { pub fn new(reader: R) -> Self { Self { inner: Inner::::new(reader), } } } pub(crate) struct Inner { buf: Vec, read: R, phantom: PhantomData, } impl Inner { pub fn new(reader: T) -> Self { Self { buf: Vec::with_capacity(INIT_BUF_SIZE), read: reader, phantom: PhantomData, } } } impl Inner where T: Read, { #[inline] fn read_buf(&mut self, to_read: usize) -> std::io::Result { self.buf.reserve(to_read); let n = self .read .by_ref() .take(to_read as u64) .read_to_end(self.buf.as_mut())?; if n == 0 { return Err(std::io::ErrorKind::UnexpectedEof.into()); } Ok(n) } #[inline] fn skip_by_read(&mut self, n: usize) -> std::io::Result<()> { self.buf.reserve(n); match (&mut self.read).take(n as u64).read_to_end(&mut self.buf) { Ok(x) => { if x == n { self.buf.clear(); Ok(()) } else { Err(std::io::ErrorKind::UnexpectedEof.into()) } } Err(e) => Err(e), } } } nom-exif-2.5.4/src/loader.rs000064400000000000000000000043611046102023000137650ustar 00000000000000use std::{ cmp::{max, min}, io::Cursor, }; use crate::error::{ParsedError, ParsingError}; mod sync; pub(crate) use sync::BufLoader; const INIT_BUF_SIZE: usize = 4096; const MIN_GROW_SIZE: usize = 2 * 4096; const MAX_GROW_SIZE: usize = 10 * 4096; pub(crate) trait BufLoad { fn buf(&self) -> &[u8]; fn buf_mut(&mut self) -> &mut Vec; fn into_vec(self) -> Vec; #[allow(unused)] fn cursor(&self, idx: Idx) -> Cursor<&[u8]> where Idx: std::slice::SliceIndex<[u8], Output = [u8]>, { Cursor::new(&self.buf()[idx]) } fn clear(&mut self) { self.buf_mut().clear(); } } pub(crate) trait Load: BufLoad { fn read_buf(&mut self, n: usize) -> std::io::Result; fn skip(&mut self, n: usize) -> std::io::Result<()>; fn load_and_parse(&mut self, mut parse: P) -> Result where P: FnMut(&[u8]) -> Result, { self.load_and_parse_at(|x, _| parse(x), 0) } #[tracing::instrument(skip_all)] fn load_and_parse_at(&mut self, mut parse: P, at: usize) -> Result where P: FnMut(&[u8], usize) -> Result, { if at >= self.buf().len() { self.read_buf(INIT_BUF_SIZE)?; } loop { match parse(self.buf(), at) { Ok(o) => return Ok(o), Err(ParsingError::ClearAndSkip(n)) => { tracing::debug!(n, "clear and skip bytes"); self.skip(n - self.buf().len())?; self.clear(); self.read_buf(INIT_BUF_SIZE)?; } Err(ParsingError::Need(i)) => { tracing::debug!(need = i, "need more bytes"); let to_read = max(i, MIN_GROW_SIZE); let to_read = min(to_read, MAX_GROW_SIZE); let n = self.read_buf(to_read)?; if n == 0 { return Err(ParsedError::NoEnoughBytes); } tracing::debug!(actual_read = n, "has been read"); } Err(ParsingError::Failed(s)) => return Err(ParsedError::Failed(s)), } } } } nom-exif-2.5.4/src/mov.rs000064400000000000000000000460371046102023000133260ustar 00000000000000use std::{ collections::BTreeMap, io::{Read, Seek}, ops::Range, }; use chrono::DateTime; use nom::{bytes::streaming, IResult}; use crate::{bbox::to_boxes, values::filter_zero}; #[allow(deprecated)] use crate::{ bbox::{ find_box, parse_video_tkhd_in_moov, travel_header, IlstBox, KeysBox, MvhdBox, ParseBox, }, error::ParsingError, loader::{BufLoader, Load}, partial_vec::PartialVec, skip::Seekable, video::TrackInfoTag, EntryValue, FileFormat, }; /// *Deprecated*: Please use [`MediaParser`] instead. /// /// Analyze the byte stream in the `reader` as a MOV/MP4 file, attempting to /// extract any possible metadata it may contain, and return it in the form of /// key-value pairs. /// /// Please note that the parsing routine itself provides a buffer, so the /// `reader` may not need to be wrapped with `BufRead`. /// /// # Usage /// /// ```rust /// use nom_exif::*; /// /// use std::fs::File; /// use std::path::Path; /// /// let f = File::open(Path::new("./testdata/meta.mov")).unwrap(); /// let entries = parse_metadata(f).unwrap(); /// /// assert_eq!( /// entries /// .iter() /// .map(|x| format!("{x:?}")) /// .collect::>() /// .join("\n"), /// r#"("com.apple.quicktime.make", Text("Apple")) /// ("com.apple.quicktime.model", Text("iPhone X")) /// ("com.apple.quicktime.software", Text("12.1.2")) /// ("com.apple.quicktime.location.ISO6709", Text("+27.1281+100.2508+000.000/")) /// ("com.apple.quicktime.creationdate", Time(2019-02-12T15:27:12+08:00)) /// ("duration", U32(500)) /// ("width", U32(720)) /// ("height", U32(1280))"#, /// ); /// ``` #[deprecated(since = "2.0.0")] #[tracing::instrument(skip_all)] #[allow(deprecated)] pub fn parse_metadata(reader: R) -> crate::Result> { let mut loader = BufLoader::::new(reader); let ff = FileFormat::try_from_load(&mut loader)?; match ff { FileFormat::Jpeg | FileFormat::Heif => { return Err(crate::error::Error::ParseFailed( "can not parse metadata from an image".into(), )); } FileFormat::QuickTime | FileFormat::MP4 => (), FileFormat::Ebml => { return Err(crate::error::Error::ParseFailed( "please use MediaParser to parse *.webm, *.mkv files".into(), )) } }; let moov_body = extract_moov_body(loader)?; let (_, mut entries) = match parse_moov_body(&moov_body) { Ok((remain, Some(entries))) => (remain, entries), Ok((remain, None)) => (remain, Vec::new()), Err(_) => { return Err("invalid moov body".into()); } }; let map: BTreeMap = convert_video_tags(entries.clone()); let mut extras = parse_mvhd_tkhd(&moov_body); const CREATIONDATE_KEY: &str = "com.apple.quicktime.creationdate"; if map.contains_key(&TrackInfoTag::CreateDate) { extras.remove(&TrackInfoTag::CreateDate); let date = map.get(&TrackInfoTag::CreateDate); if let Some(pos) = entries.iter().position(|x| x.0 == CREATIONDATE_KEY) { if let Some(date) = date { entries[pos] = (CREATIONDATE_KEY.to_string(), date.clone()); } else { entries.remove(pos); } } } entries.extend(extras.into_iter().map(|(k, v)| match k { TrackInfoTag::ImageWidth => ("width".to_string(), v), TrackInfoTag::ImageHeight => ("height".to_string(), v), TrackInfoTag::DurationMs => ( "duration".to_string(), // For compatibility with older versions, convert to u32 EntryValue::U32(v.as_u64().unwrap() as u32), ), TrackInfoTag::CreateDate => (CREATIONDATE_KEY.to_string(), v), _ => unreachable!(), })); if map.contains_key(&TrackInfoTag::GpsIso6709) { const LOCATION_KEY: &str = "com.apple.quicktime.location.ISO6709"; if let Some(idx) = entries.iter().position(|(k, _)| k == "udta.ยฉxyz") { entries.remove(idx); entries.push(( LOCATION_KEY.to_string(), map.get(&TrackInfoTag::GpsIso6709).unwrap().to_owned(), )); } } Ok(entries) } #[tracing::instrument(skip_all)] pub(crate) fn parse_qt( moov_body: &[u8], ) -> Result, ParsingError> { let (_, entries) = match parse_moov_body(moov_body) { Ok((remain, Some(entries))) => (remain, entries), Ok((remain, None)) => (remain, Vec::new()), Err(_) => { return Err("invalid moov body".into()); } }; let mut entries: BTreeMap = convert_video_tags(entries); let extras = parse_mvhd_tkhd(moov_body); if entries.contains_key(&TrackInfoTag::CreateDate) { entries.remove(&TrackInfoTag::CreateDate); } entries.extend(extras); Ok(entries) } #[tracing::instrument(skip_all)] pub(crate) fn parse_mp4( moov_body: &[u8], ) -> Result, ParsingError> { let (_, entries) = match parse_moov_body(moov_body) { Ok((remain, Some(entries))) => (remain, entries), Ok((remain, None)) => (remain, Vec::new()), Err(_) => { return Err("invalid moov body".into()); } }; let mut entries: BTreeMap = convert_video_tags(entries); let extras = parse_mvhd_tkhd(moov_body); entries.extend(extras); Ok(entries) } fn parse_mvhd_tkhd(moov_body: &[u8]) -> BTreeMap { let mut entries = BTreeMap::new(); if let Ok((_, Some(bbox))) = find_box(moov_body, "mvhd") { if let Ok((_, mvhd)) = MvhdBox::parse_box(bbox.data) { entries.insert(TrackInfoTag::DurationMs, mvhd.duration_ms().into()); entries.insert( TrackInfoTag::CreateDate, EntryValue::Time(mvhd.creation_time()), ); } } if let Ok(Some(tkhd)) = parse_video_tkhd_in_moov(moov_body) { entries.insert(TrackInfoTag::ImageWidth, tkhd.width.into()); entries.insert(TrackInfoTag::ImageHeight, tkhd.height.into()); } entries } fn convert_video_tags(entries: Vec<(String, EntryValue)>) -> BTreeMap { entries .into_iter() .filter_map(|(k, v)| { if k == "com.apple.quicktime.creationdate" { v.as_str() .and_then(|s| DateTime::parse_from_str(s, "%+").ok()) .map(|t| (TrackInfoTag::CreateDate, EntryValue::Time(t))) } else if k == "com.apple.quicktime.make" { Some((TrackInfoTag::Make, v)) } else if k == "com.apple.quicktime.model" { Some((TrackInfoTag::Model, v)) } else if k == "com.apple.quicktime.software" { Some((TrackInfoTag::Software, v)) } else if k == "com.apple.quicktime.author" { Some((TrackInfoTag::Author, v)) } else if k == "com.apple.quicktime.location.ISO6709" { Some((TrackInfoTag::GpsIso6709, v)) } else if k == "udta.ยฉxyz" { // For mp4 files, Android phones store GPS info in that box. v.as_u8array() .and_then(parse_udta_gps) .map(|v| (TrackInfoTag::GpsIso6709, EntryValue::Text(v))) } else if k == "udta.auth" { v.as_u8array() .and_then(parse_udta_auth) .map(|v| (TrackInfoTag::Author, EntryValue::Text(v))) } else if k.starts_with("udta.") { let tag = TryInto::::try_into(k.as_str()).ok(); tag.map(|t| (t, v)) } else { None } }) .collect() } /// Try to find GPS info from box `moov/udta/ยฉxyz`. For mp4 files, Android /// phones store GPS info in that box. // fn parse_mp4_gps(moov_body: &[u8]) -> Option { // let bbox = match find_box(moov_body, "udta/ยฉxyz") { // Ok((_, b)) => b, // Err(_) => None, // }; // if let Some(bbox) = bbox { // return parse_udta_gps(bbox.body_data()); // } // None // } fn parse_udta_gps(data: &[u8]) -> Option { if data.len() <= 4 { tracing::warn!("moov/udta/ยฉxyz body is too small"); None } else { // The first 4 bytes is zero, skip them let location = data[4..] // Safe-slice .iter() .map(|b| *b as char) .collect::(); Some(location) } } const ISO_639_2_UND: [u8; 2] = [0x55, 0xc4]; fn parse_udta_auth(data: &[u8]) -> Option { // Skip leading zero bytes let data = filter_zero(data); // Skip leading language flags. // Refer to: https://exiftool.org/forum/index.php?topic=11498.0 if data.starts_with(&ISO_639_2_UND) { String::from_utf8(data.into_iter().skip(2).collect()).ok() } else { String::from_utf8(data).ok() } } /// *Deprecated*: Please use [`crate::MediaParser`] instead. /// /// Analyze the byte stream in the `reader` as a MOV file, attempting to extract /// any possible metadata it may contain, and return it in the form of key-value /// pairs. /// /// Please note that the parsing routine itself provides a buffer, so the /// `reader` may not need to be wrapped with `BufRead`. /// /// # Usage /// /// ```rust /// use nom_exif::*; /// /// use std::fs::File; /// use std::path::Path; /// /// let f = File::open(Path::new("./testdata/meta.mov")).unwrap(); /// let entries = parse_mov_metadata(f).unwrap(); /// /// assert_eq!( /// entries /// .iter() /// .map(|x| format!("{x:?}")) /// .collect::>() /// .join("\n"), /// r#"("com.apple.quicktime.make", Text("Apple")) /// ("com.apple.quicktime.model", Text("iPhone X")) /// ("com.apple.quicktime.software", Text("12.1.2")) /// ("com.apple.quicktime.location.ISO6709", Text("+27.1281+100.2508+000.000/")) /// ("com.apple.quicktime.creationdate", Time(2019-02-12T15:27:12+08:00)) /// ("duration", U32(500)) /// ("width", U32(720)) /// ("height", U32(1280))"#, /// ); /// ``` #[deprecated(since = "2.0.0")] pub fn parse_mov_metadata(reader: R) -> crate::Result> { #[allow(deprecated)] parse_metadata(reader) } #[tracing::instrument(skip_all)] fn extract_moov_body(mut loader: L) -> Result { let moov_body_range = loader.load_and_parse(extract_moov_body_from_buf)?; tracing::debug!(?moov_body_range); Ok(PartialVec::from_vec_range( loader.into_vec(), moov_body_range, )) } /// Parse the byte data of an ISOBMFF file and return the potential body data of /// moov atom it may contain. /// /// Regarding error handling, please refer to [Error] for more information. #[tracing::instrument(skip_all)] pub(crate) fn extract_moov_body_from_buf(input: &[u8]) -> Result, ParsingError> { // parse metadata from moov/meta/keys & moov/meta/ilst let remain = input; let convert_error = |e: nom::Err<_>, msg: &str| match e { nom::Err::Incomplete(needed) => match needed { nom::Needed::Unknown => ParsingError::Need(1), nom::Needed::Size(n) => ParsingError::Need(n.get()), }, nom::Err::Failure(_) | nom::Err::Error(_) => ParsingError::Failed(msg.to_string()), }; let mut to_skip = 0; let mut skipped = 0; let (remain, header) = travel_header(remain, |h, remain| { tracing::debug!(?h.box_type, ?h.box_size, "Got"); if h.box_type == "moov" { // stop travelling skipped += h.header_size; false } else if (remain.len() as u64) < h.body_size() { // stop travelling & skip unused box data to_skip = h.body_size() as usize - remain.len(); false } else { // body has been read, so just consume it skipped += h.box_size as usize; true } }) .map_err(|e| convert_error(e, "search atom moov failed"))?; if to_skip > 0 { return Err(ParsingError::ClearAndSkip( to_skip .checked_add(input.len()) .ok_or_else(|| ParsingError::Failed("to_skip is too big".into()))?, )); } let size: usize = header.body_size().try_into().expect("must fit"); let (_, body) = streaming::take(size)(remain).map_err(|e| convert_error(e, "moov is too small"))?; Ok(skipped..skipped + body.len()) } type EntriesResult<'a> = IResult<&'a [u8], Option>>; #[tracing::instrument(skip(input))] fn parse_moov_body(input: &[u8]) -> EntriesResult { tracing::debug!("parse_moov_body"); let mut entries = parse_meta(input).unwrap_or_default(); if let Ok((_, Some(udta))) = find_box(input, "udta") { tracing::debug!("udta"); if let Ok(boxes) = to_boxes(udta.body_data()) { for entry in boxes.iter() { tracing::debug!(?entry, "udta entry"); entries.push(( format!("udta.{}", entry.box_type()), EntryValue::U8Array(Vec::from(entry.body_data())), )); } } } Ok((input, Some(entries))) } fn parse_meta(input: &[u8]) -> Option> { let (_, Some(meta)) = find_box(input, "meta").ok()? else { return None; }; let (_, Some(keys)) = find_box(meta.body_data(), "keys").ok()? else { return None; }; let (_, Some(ilst)) = find_box(meta.body_data(), "ilst").ok()? else { return None; }; let (_, keys) = KeysBox::parse_box(keys.data).ok()?; let (_, ilst) = IlstBox::parse_box(ilst.data).ok()?; let entries = keys .entries .into_iter() .map(|k| k.key) .zip(ilst.items.into_iter().map(|v| v.value)) .collect::>(); Some(entries) } /// Change timezone format from iso 8601 to rfc3339, e.g.: /// /// - `2023-11-02T19:58:34+08` -> `2023-11-02T19:58:34+08:00` /// - `2023-11-02T19:58:34+0800` -> `2023-11-02T19:58:34+08:00` #[allow(dead_code)] fn tz_iso_8601_to_rfc3339(s: String) -> String { use regex::Regex; let ss = s.trim(); // Safe unwrap let re = Regex::new(r"([+-][0-9][0-9])([0-9][0-9])?$").unwrap(); if let Some((offset, tz)) = re.captures(ss).map(|caps| { ( // Safe unwrap caps.get(1).unwrap().start(), format!( "{}:{}", caps.get(1).map_or("00", |m| m.as_str()), caps.get(2).map_or("00", |m| m.as_str()) ), ) }) { let s1 = &ss.as_bytes()[..offset]; // Safe-slice let s2 = tz.as_bytes(); s1.iter().chain(s2.iter()).map(|x| *x as char).collect() } else { s } } #[cfg(test)] #[allow(deprecated)] mod tests { use super::*; use crate::testkit::*; use test_case::test_case; #[test_case("meta.mov")] fn mov_parse(path: &str) { let reader = open_sample(path).unwrap(); let entries = parse_metadata(reader).unwrap(); assert_eq!( entries .iter() .map(|x| format!("{x:?}")) .collect::>() .join("\n"), "(\"com.apple.quicktime.make\", Text(\"Apple\")) (\"com.apple.quicktime.model\", Text(\"iPhone X\")) (\"com.apple.quicktime.software\", Text(\"12.1.2\")) (\"com.apple.quicktime.location.ISO6709\", Text(\"+27.1281+100.2508+000.000/\")) (\"com.apple.quicktime.creationdate\", Time(2019-02-12T15:27:12+08:00)) (\"duration\", U32(500)) (\"width\", U32(720)) (\"height\", U32(1280))" ); } #[test_case("meta.mov")] fn mov_extract_mov(path: &str) { let _ = tracing_subscriber::fmt().with_test_writer().try_init(); let buf = read_sample(path).unwrap(); tracing::info!(bytes = buf.len(), "File size."); let range = extract_moov_body_from_buf(&buf).unwrap(); let (_, entries) = parse_moov_body(&buf[range]).unwrap(); assert_eq!( entries .unwrap() .iter() .map(|x| format!("{x:?}")) .collect::>() .join("\n"), "(\"com.apple.quicktime.make\", Text(\"Apple\")) (\"com.apple.quicktime.model\", Text(\"iPhone X\")) (\"com.apple.quicktime.software\", Text(\"12.1.2\")) (\"com.apple.quicktime.location.ISO6709\", Text(\"+27.1281+100.2508+000.000/\")) (\"com.apple.quicktime.creationdate\", Text(\"2019-02-12T15:27:12+08:00\"))" ); } #[test_case("meta.mp4")] fn parse_mp4(path: &str) { let _ = tracing_subscriber::fmt().with_test_writer().try_init(); let entries = parse_metadata(open_sample(path).unwrap()).unwrap(); assert_eq!( entries .iter() .map(|x| format!("{x:?}")) .collect::>() .join("\n"), "(\"com.apple.quicktime.creationdate\", Time(2024-02-03T07:05:38+00:00)) (\"duration\", U32(1063)) (\"width\", U32(1920)) (\"height\", U32(1080)) (\"com.apple.quicktime.location.ISO6709\", Text(\"+27.2939+112.6932/\"))" ); } #[test_case("embedded-in-heic.mov")] fn parse_embedded_mov(path: &str) { let _ = tracing_subscriber::fmt().with_test_writer().try_init(); let entries = parse_mov_metadata(open_sample(path).unwrap()).unwrap(); assert_eq!( entries .iter() .map(|x| format!("{x:?}")) .collect::>() .join("\n"), "(\"com.apple.quicktime.location.accuracy.horizontal\", Text(\"14.235563\")) (\"com.apple.quicktime.live-photo.auto\", U8(1)) (\"com.apple.quicktime.content.identifier\", Text(\"DA1A7EE8-0925-4C9F-9266-DDA3F0BB80F0\")) (\"com.apple.quicktime.live-photo.vitality-score\", F32(0.93884003)) (\"com.apple.quicktime.live-photo.vitality-scoring-version\", I64(4)) (\"com.apple.quicktime.location.ISO6709\", Text(\"+22.5797+113.9380+028.396/\")) (\"com.apple.quicktime.make\", Text(\"Apple\")) (\"com.apple.quicktime.model\", Text(\"iPhone 15 Pro\")) (\"com.apple.quicktime.software\", Text(\"17.1\")) (\"com.apple.quicktime.creationdate\", Time(2023-11-02T19:58:34+08:00)) (\"duration\", U32(2795)) (\"width\", U32(1920)) (\"height\", U32(1440))" ); } #[test] fn test_iso_8601_tz_to_rfc3339() { let _ = tracing_subscriber::fmt().with_test_writer().try_init(); let s = "2023-11-02T19:58:34+08".to_string(); assert_eq!(tz_iso_8601_to_rfc3339(s), "2023-11-02T19:58:34+08:00"); let s = "2023-11-02T19:58:34+0800".to_string(); assert_eq!(tz_iso_8601_to_rfc3339(s), "2023-11-02T19:58:34+08:00"); let s = "2023-11-02T19:58:34+08:00".to_string(); assert_eq!(tz_iso_8601_to_rfc3339(s), "2023-11-02T19:58:34+08:00"); let s = "2023-11-02T19:58:34Z".to_string(); assert_eq!(tz_iso_8601_to_rfc3339(s), "2023-11-02T19:58:34Z"); let s = "2023-11-02T19:58:34".to_string(); assert_eq!(tz_iso_8601_to_rfc3339(s), "2023-11-02T19:58:34"); } } nom-exif-2.5.4/src/parser.rs000064400000000000000000000502451046102023000140150ustar 00000000000000use std::{ cmp::{max, min}, fmt::{Debug, Display}, fs::File, io::{self, Read, Seek}, marker::PhantomData, net::TcpStream, ops::Range, path::Path, }; use crate::{ buffer::Buffers, error::{ParsedError, ParsingError, ParsingErrorState}, exif::{parse_exif_iter, TiffHeader}, file::Mime, partial_vec::PartialVec, skip::Skip, video::parse_track_info, ExifIter, Seekable, TrackInfo, Unseekable, }; /// `MediaSource` represents a media data source that can be parsed by /// [`MediaParser`]. /// /// - Use `MediaSource::file_path(path)` or `MediaSource::file(file)` to create /// a MediaSource from a file /// /// - Use `MediaSource::tcp_stream(stream)` to create a MediaSource from a `TcpStream` /// - In other cases: /// /// - Use `MediaSource::seekable(reader)` to create a MediaSource from a `Read + Seek` /// /// - Use `MediaSource::unseekable(reader)` to create a MediaSource from a /// reader that only impl `Read` /// /// `seekable` is preferred to `unseekable`, since the former is more efficient /// when the parser needs to skip a large number of bytes. /// /// Passing in a `BufRead` should be avoided because [`MediaParser`] comes with /// its own buffer management and the buffers can be shared between multiple /// parsing tasks, thus avoiding frequent memory allocations. pub struct MediaSource { pub(crate) reader: R, pub(crate) buf: Vec, pub(crate) mime: Mime, phantom: PhantomData, } impl> Debug for MediaSource { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("MediaSource") // .field("reader", &self.reader) .field("mime", &self.mime) .field("seekable", &S::debug()) .finish_non_exhaustive() } } // Should be enough for parsing header const HEADER_PARSE_BUF_SIZE: usize = 128; impl> MediaSource { #[tracing::instrument(skip(reader))] fn build(mut reader: R) -> crate::Result { // TODO: reuse MediaParser to parse header 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: Mime = buf.as_slice().try_into()?; tracing::debug!(?mime); Ok(Self { reader, buf, mime, phantom: PhantomData, }) } pub fn has_track(&self) -> bool { match self.mime { Mime::Image(_) => false, Mime::Video(_) => true, } } pub fn has_exif(&self) -> bool { match self.mime { Mime::Image(_) => true, Mime::Video(_) => false, } } } impl MediaSource { pub fn seekable(reader: R) -> crate::Result { Self::build(reader) } } impl MediaSource { pub fn unseekable(reader: R) -> crate::Result { Self::build(reader) } } impl MediaSource { pub fn file_path>(path: P) -> crate::Result { Self::seekable(File::open(path)?) } pub fn file(file: File) -> crate::Result { Self::seekable(file) } } impl MediaSource { pub fn tcp_stream(stream: TcpStream) -> crate::Result { Self::unseekable(stream) } } // Keep align with 4K pub(crate) const INIT_BUF_SIZE: usize = 4096; pub(crate) const MIN_GROW_SIZE: usize = 4096; // Max size of APP1 is 0xFFFF // pub(crate) const MAX_GROW_SIZE: usize = 63 * 1024; // Set a reasonable upper limit for single buffer allocation. pub(crate) const MAX_ALLOC_SIZE: usize = 1024 * 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, Clone)] pub(crate) enum ParsingState { TiffHeader(TiffHeader), HeifExifSize(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), } } } pub(crate) trait BufParser: Buf + Debug { fn fill_buf(&mut self, reader: &mut R, size: usize) -> io::Result; fn load_and_parse, P, O>( &mut self, reader: &mut R, mut parse: P, ) -> Result where P: FnMut(&[u8], Option) -> Result, { self.load_and_parse_with_offset::( reader, |data, _, state| parse(data, state), 0, ) } #[tracing::instrument(skip_all)] fn load_and_parse_with_offset, P, O>( &mut self, reader: &mut R, mut parse: P, offset: usize, ) -> Result where P: FnMut(&[u8], usize, Option) -> Result, { if offset >= self.buffer().len() { self.fill_buf(reader, MIN_GROW_SIZE)?; } let mut parsing_state: Option = None; loop { let res = parse(self.buffer(), offset, parsing_state.take()); match res { Ok(o) => return Ok(o), Err(es) => { tracing::debug!(?es); parsing_state = es.state; match es.err { ParsingError::ClearAndSkip(n) => { self.clear_and_skip::(reader, n)?; } ParsingError::Need(i) => { tracing::debug!(need = i, "need more bytes"); let to_read = max(i, MIN_GROW_SIZE); // let to_read = min(to_read, MAX_GROW_SIZE); let n = self.fill_buf(reader, to_read)?; if n == 0 { return Err(ParsedError::NoEnoughBytes); } tracing::debug!(n, "actual read"); } ParsingError::Failed(s) => return Err(ParsedError::Failed(s)), } } } } } #[tracing::instrument(skip(reader))] fn clear_and_skip>( &mut self, reader: &mut R, n: usize, ) -> Result<(), ParsedError> { tracing::debug!("ClearAndSkip"); if n <= self.buffer().len() { tracing::debug!(n, "skip by set_position"); self.set_position(n); return Ok(()); } let skip_n = n - self.buffer().len(); tracing::debug!(skip_n, "clear and skip bytes"); self.clear(); let done = S::skip_by_seek( reader, skip_n .try_into() .map_err(|_| ParsedError::Failed("skip too many bytes".into()))?, )?; if !done { tracing::debug!(skip_n, "skip by using our buffer"); let mut skipped = 0; while skipped < skip_n { let mut to_skip = skip_n - skipped; to_skip = min(to_skip, MAX_ALLOC_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; } } } else { tracing::debug!(skip_n, "skip with seek"); } 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.buf().len()))] fn fill_buf(&mut self, reader: &mut R, size: usize) -> io::Result { if size.saturating_add(self.buf().len()) > MAX_ALLOC_SIZE { tracing::error!(?size, "the requested buffer size is too big"); return Err(io::ErrorKind::Unsupported.into()); } self.buf_mut().reserve_exact(size); let n = reader.take(size as u64).read_to_end(self.buf_mut())?; if n == 0 { tracing::error!(buf_len = self.buf().len(), "fill_buf: EOF"); return Err(std::io::ErrorKind::UnexpectedEof.into()); } tracing::debug!( ?size, ?n, buf_len = self.buf().len(), "fill_buf: read bytes" ); Ok(n) } } impl Buf for MediaParser { fn buffer(&self) -> &[u8] { &self.buf()[self.position..] } fn clear(&mut self) { self.buf_mut().clear(); } fn set_position(&mut self, pos: usize) { self.position = pos; } fn position(&self) -> usize { self.position } } pub trait ParseOutput: Sized { fn parse(parser: &mut MediaParser, ms: MediaSource) -> crate::Result; } impl> ParseOutput for ExifIter { fn parse(parser: &mut MediaParser, mut ms: MediaSource) -> crate::Result { if !ms.has_exif() { return Err(crate::Error::ParseFailed("no Exif data here".into())); } parse_exif_iter::(parser, ms.mime.unwrap_image(), &mut ms.reader) } } impl> ParseOutput for TrackInfo { fn parse(parser: &mut MediaParser, mut ms: MediaSource) -> crate::Result { if !ms.has_track() { return Err(crate::Error::ParseFailed("no track info here".into())); } let out = parser.load_and_parse::(ms.reader.by_ref(), |data, _| { parse_track_info(data, ms.mime.unwrap_video()) .map_err(|e| ParsingErrorState::new(e, None)) })?; Ok(out) } } /// A `MediaParser`/`AsyncMediaParser` can parse media info from a /// [`MediaSource`]. /// /// `MediaParser`/`AsyncMediaParser` manages inner parse buffers that can be /// shared between multiple parsing tasks, thus avoiding frequent memory /// allocations. /// /// Therefore: /// /// - Try to reuse a `MediaParser`/`AsyncMediaParser` instead of creating a new /// one every time you need it. /// /// - `MediaSource` should be created directly from `Read`, not from `BufRead`. /// /// ## Example /// /// ```rust /// use nom_exif::*; /// use chrono::DateTime; /// /// let mut parser = MediaParser::new(); /// /// // ------------------- Parse Exif Info /// let ms = MediaSource::file_path("./testdata/exif.heic").unwrap(); /// assert!(ms.has_exif()); /// let mut iter: ExifIter = parser.parse(ms).unwrap(); /// /// let entry = iter.next().unwrap(); /// assert_eq!(entry.tag().unwrap(), ExifTag::Make); /// assert_eq!(entry.get_value().unwrap().as_str().unwrap(), "Apple"); /// /// // Convert `ExifIter` into an `Exif`. Clone it before converting, so that /// // we can start the iteration from the beginning. /// let exif: Exif = iter.clone().into(); /// assert_eq!(exif.get(ExifTag::Make).unwrap().as_str().unwrap(), "Apple"); /// /// // ------------------- Parse Track Info /// let ms = MediaSource::file_path("./testdata/meta.mov").unwrap(); /// assert!(ms.has_track()); /// let info: TrackInfo = parser.parse(ms).unwrap(); /// /// assert_eq!(info.get(TrackInfoTag::Make), Some(&"Apple".into())); /// assert_eq!(info.get(TrackInfoTag::Model), Some(&"iPhone X".into())); /// assert_eq!(info.get(TrackInfoTag::GpsIso6709), Some(&"+27.1281+100.2508+000.000/".into())); /// assert_eq!(info.get_gps_info().unwrap().latitude_ref, 'N'); /// assert_eq!( /// info.get_gps_info().unwrap().latitude, /// [(27, 1), (7, 1), (68, 100)].into(), /// ); /// ``` pub struct MediaParser { bb: Buffers, buf: Option>, position: usize, } impl Debug for MediaParser { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("MediaParser") .field("buffers", &self.bb) .field("buf len", &self.buf.as_ref().map(|x| x.len())) .field("position", &self.position) .finish_non_exhaustive() } } impl Default for MediaParser { fn default() -> Self { Self { bb: Buffers::new(), buf: None, position: 0, } } } pub(crate) trait ShareBuf { fn share_buf(&mut self, range: Range) -> PartialVec; } impl ShareBuf for MediaParser { fn share_buf(&mut self, mut range: Range) -> PartialVec { let buf = self.buf.take().unwrap(); let vec = self.bb.release_to_share(buf); range.start += self.position; range.end += self.position; PartialVec::new(vec, range) } } impl MediaParser { pub fn new() -> Self { Self::default() } /// `MediaParser`/`AsyncMediaParser` comes with its own buffer management, /// so that buffers can be reused during multiple parsing processes to /// avoid frequent memory allocations. Therefore, try to reuse a /// `MediaParser` instead of creating a new one every time you need it. /// /// **Note**: /// /// - For [`ExifIter`] as parse output, Please avoid holding the `ExifIter` /// object all the time and drop it immediately after use. Otherwise, the /// parsing buffer referenced by the `ExifIter` object will not be reused /// by [`MediaParser`], resulting in repeated memory allocation in the /// subsequent parsing process. /// /// If you really need to retain some data, please take out the required /// Entry values โ€‹โ€‹and save them, or convert the `ExifIter` into an /// [`crate::Exif`] object to retain all Entry values. /// /// - For [`TrackInfo`] as parse output, you don't need to worry about /// this, because `TrackInfo` dosn't reference the parsing buffer. pub fn parse>( &mut self, mut ms: MediaSource, ) -> crate::Result { self.reset(); self.acquire_buf(); self.buf_mut().append(&mut ms.buf); let res = self.do_parse(ms); self.reset(); res } fn do_parse>( &mut self, mut ms: MediaSource, ) -> Result { self.fill_buf(&mut ms.reader, INIT_BUF_SIZE)?; let res = ParseOutput::parse(self, ms)?; Ok(res) } fn reset(&mut self) { // Ensure buf has been released if let Some(buf) = self.buf.take() { self.bb.release(buf); } // Reset position self.set_position(0); } pub(crate) fn buf(&self) -> &Vec { match self.buf.as_ref() { Some(b) => b, None => panic!("no buf here"), } } fn buf_mut(&mut self) -> &mut Vec { match self.buf.as_mut() { Some(b) => b, None => panic!("no buf here"), } } fn acquire_buf(&mut self) { assert!(self.buf.is_none()); self.buf = Some(self.bb.acquire()); } } #[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> = 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::file_path(Path::new("testdata").join(path)); match te { Track => { let ms = ms.unwrap(); // println!("path: {path} mime: {:?}", ms.mime); assert!(ms.has_track()); let _: TrackInfo = parser.parse(ms).unwrap(); } Exif => { let ms = ms.unwrap(); // println!("path: {path} mime: {:?}", ms.mime); assert!(ms.has_exif()); let mut it: ExifIter = parser.parse(ms).unwrap(); let _ = it.parse_gps_info(); if path.contains("one-entry") { assert!(it.next().is_some()); assert!(it.next().is_none()); let exif: crate::Exif = it.clone_and_rewind().into(); assert!(exif.get(ExifTag::Orientation).is_some()); } else { let _: crate::Exif = it.clone_and_rewind().into(); } } NoData => { let ms = ms.unwrap(); // println!("path: {path} mime: {:?}", ms.mime); if ms.has_exif() { let res: Result = parser.parse(ms); res.unwrap_err(); } else if ms.has_track() { let res: Result = parser.parse(ms); res.unwrap_err(); } } Invalid => { ms.unwrap_err(); } } } use crate::testkit::open_sample; use crate::{EntryValue, ExifTag, TrackInfoTag}; use chrono::DateTime; use test_case::test_case; use crate::video::TrackInfoTag::*; #[test_case("mkv_640x360.mkv", ImageWidth, 640_u32.into())] #[test_case("mkv_640x360.mkv", ImageHeight, 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.mp4", ImageWidth, 1920_u32.into())] #[test_case("meta.mp4", ImageHeight, 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")] fn parse_track_info(path: &str, tag: TrackInfoTag, v: EntryValue) { let mut parser = parser(); let mf = MediaSource::file(open_sample(path).unwrap()).unwrap(); let info: TrackInfo = parser.parse(mf).unwrap(); assert_eq!(info.get(tag).unwrap(), &v); let mf = MediaSource::unseekable(open_sample(path).unwrap()).unwrap(); let info: TrackInfo = parser.parse(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::file(open_sample(path).unwrap()).unwrap(); let _: TrackInfo = parser.parse(mf).unwrap_or_default(); let mf = MediaSource::unseekable(open_sample(path).unwrap()).unwrap(); let _: TrackInfo = parser.parse(mf).unwrap_or_default(); } } nom-exif-2.5.4/src/parser_async.rs000064400000000000000000000436051046102023000152140ustar 00000000000000use std::{ cmp::{max, min}, fmt::Debug, io::{self}, marker::PhantomData, ops::Range, path::Path, }; use tokio::{ fs::File, io::{AsyncRead, AsyncReadExt, AsyncSeek}, }; use crate::{ buffer::Buffers, error::{ParsedError, ParsingError, ParsingErrorState}, exif::parse_exif_iter_async, file::Mime, parser::{Buf, ParsingState, ShareBuf, INIT_BUF_SIZE, MAX_ALLOC_SIZE, MIN_GROW_SIZE}, partial_vec::PartialVec, skip::AsyncSkip, video::parse_track_info, ExifIter, Seekable, TrackInfo, Unseekable, }; // Should be enough for parsing header const HEADER_PARSE_BUF_SIZE: usize = 128; pub struct AsyncMediaSource { pub(crate) reader: R, pub(crate) buf: Vec, pub(crate) mime: Mime, phantom: PhantomData, } impl> AsyncMediaSource { async fn build(mut reader: R) -> crate::Result { // TODO: reuse MediaParser to parse header let mut buf = Vec::with_capacity(HEADER_PARSE_BUF_SIZE); (&mut reader) .take(HEADER_PARSE_BUF_SIZE as u64) .read_to_end(&mut buf) .await?; let mime: Mime = buf.as_slice().try_into()?; Ok(Self { reader, buf, mime, phantom: PhantomData, }) } pub fn has_track(&self) -> bool { match self.mime { Mime::Image(_) => false, Mime::Video(_) => true, } } pub fn has_exif(&self) -> bool { match self.mime { Mime::Image(_) => true, Mime::Video(_) => false, } } } impl AsyncMediaSource { pub async fn seekable(reader: R) -> crate::Result { Self::build(reader).await } } impl AsyncMediaSource { pub async fn unseekable(reader: R) -> crate::Result { Self::build(reader).await } } impl AsyncMediaSource { pub async fn file(reader: File) -> crate::Result { Self::build(reader).await } pub async fn file_path>(path: P) -> crate::Result { Self::build(File::open(path).await?).await } } pub(crate) trait AsyncBufParser: Buf + Debug { async fn fill_buf( &mut self, reader: &mut R, size: usize, ) -> io::Result; async fn load_and_parse, P, O>( &mut self, reader: &mut R, parse: P, ) -> Result where P: Fn(&[u8], Option) -> Result, { self.load_and_parse_with_offset::( reader, |data, _, state| parse(data, state), 0, ) .await } #[tracing::instrument(skip_all)] async fn load_and_parse_with_offset, P, O>( &mut self, reader: &mut R, parse: P, offset: usize, ) -> Result where P: Fn(&[u8], usize, Option) -> Result, { if offset >= self.buffer().len() { self.fill_buf(reader, MIN_GROW_SIZE).await?; } let mut parsing_state: Option = None; loop { let res = parse(self.buffer(), offset, parsing_state.take()); match res { Ok(o) => return Ok(o), Err(es) => { tracing::debug!(?es); parsing_state = es.state; match es.err { ParsingError::ClearAndSkip(n) => { self.clear_and_skip::(reader, n).await?; } ParsingError::Need(i) => { tracing::debug!(need = i, "need more bytes"); let to_read = max(i, MIN_GROW_SIZE); // let to_read = min(to_read, MAX_GROW_SIZE); let n = self.fill_buf(reader, to_read).await?; if n == 0 { return Err(ParsedError::NoEnoughBytes); } tracing::debug!(actual_read = n, "has been read"); } ParsingError::Failed(s) => return Err(ParsedError::Failed(s)), } } } } } #[tracing::instrument(skip(reader))] async fn clear_and_skip>( &mut self, reader: &mut R, n: usize, ) -> Result<(), ParsedError> { tracing::debug!("ClearAndSkip"); if n <= self.buffer().len() { tracing::debug!(n, "skip by set_position"); self.set_position(n); return Ok(()); } let skip_n = n - self.buffer().len(); tracing::debug!(skip_n, "clear and skip bytes"); self.clear(); let done = S::skip_by_seek(reader, skip_n.try_into().unwrap()).await?; if !done { tracing::debug!(skip_n, "skip by using our buffer"); let mut skipped = 0; while skipped < skip_n { let mut to_skip = skip_n - skipped; to_skip = min(to_skip, MAX_ALLOC_SIZE); let n = self.fill_buf(reader, to_skip).await?; skipped += n; if skipped <= skip_n { self.clear(); } else { let remain = skipped - skip_n; self.set_position(self.buffer().len() - remain); break; } } } else { tracing::debug!(skip_n, "skip with seek"); } if self.buffer().is_empty() { self.fill_buf(reader, MIN_GROW_SIZE).await?; } Ok(()) } } pub trait AsyncParseOutput: Sized { fn parse( parser: &mut AsyncMediaParser, ms: AsyncMediaSource, ) -> impl std::future::Future> + Send; } impl + Send> AsyncParseOutput for ExifIter { async fn parse( parser: &mut AsyncMediaParser, mut ms: AsyncMediaSource, ) -> crate::Result { if !ms.has_exif() { return Err(crate::Error::ParseFailed("no Exif data here".into())); } parse_exif_iter_async::(parser, ms.mime.unwrap_image(), &mut ms.reader).await } } impl + Send> AsyncParseOutput for TrackInfo { async fn parse( parser: &mut AsyncMediaParser, ms: AsyncMediaSource, ) -> crate::Result { let mut ms = ms; let out = match ms.mime { Mime::Image(_) => return Err("not a track".into()), Mime::Video(v) => { parser .load_and_parse::(&mut ms.reader, |data, _| { parse_track_info(data, v).map_err(|e| ParsingErrorState::new(e, None)) }) .await? } }; Ok(out) } } /// An async version of `MediaParser`. See [`crate::MediaParser`] for more /// information. /// /// ## Example /// /// ```rust /// use nom_exif::*; /// use tokio::task::spawn_blocking; /// use tokio::fs::File; /// use chrono::DateTime; /// /// #[cfg(feature = "async")] /// #[tokio::main] /// async fn main() -> Result<()> { /// let mut parser = AsyncMediaParser::new(); /// /// // ------------------- Parse Exif Info /// let ms = AsyncMediaSource::file_path("./testdata/exif.heic").await.unwrap(); /// assert!(ms.has_exif()); /// let mut iter: ExifIter = parser.parse(ms).await.unwrap(); /// /// let entry = iter.next().unwrap(); /// assert_eq!(entry.tag().unwrap(), ExifTag::Make); /// assert_eq!(entry.get_value().unwrap().as_str().unwrap(), "Apple"); /// /// // Convert `ExifIter` into an `Exif`. Clone it before converting, so that /// // we can sure the iterator state has been reset. /// let exif: Exif = iter.clone().into(); /// assert_eq!(exif.get(ExifTag::Make).unwrap().as_str().unwrap(), "Apple"); /// /// // ------------------- Parse Track Info /// let ms = AsyncMediaSource::file_path("./testdata/meta.mov").await.unwrap(); /// assert!(ms.has_track()); /// let info: TrackInfo = parser.parse(ms).await.unwrap(); /// /// assert_eq!(info.get(TrackInfoTag::Make), Some(&"Apple".into())); /// assert_eq!(info.get(TrackInfoTag::Model), Some(&"iPhone X".into())); /// assert_eq!(info.get(TrackInfoTag::GpsIso6709), Some(&"+27.1281+100.2508+000.000/".into())); /// assert_eq!(info.get_gps_info().unwrap().latitude_ref, 'N'); /// assert_eq!( /// info.get_gps_info().unwrap().latitude, /// [(27, 1), (7, 1), (68, 100)].into(), /// ); /// /// Ok(()) /// } /// ``` pub struct AsyncMediaParser { bb: Buffers, buf: Option>, position: usize, } impl Debug for AsyncMediaParser { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("MediaParser") .field("buffers", &self.bb) .field("buf len", &self.buf.as_ref().map(|x| x.len())) .field("position", &self.position) .finish_non_exhaustive() } } impl> Debug for AsyncMediaSource { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("MediaSource") // .field("reader", &self.reader) .field("mime", &self.mime) .field("seekable", &S::debug()) .finish_non_exhaustive() } } impl Default for AsyncMediaParser { fn default() -> Self { Self { bb: Buffers::new(), buf: None, position: 0, } } } impl ShareBuf for AsyncMediaParser { fn share_buf(&mut self, mut range: Range) -> PartialVec { let buf = self.buf.take().unwrap(); let vec = self.bb.release_to_share(buf); range.start += self.position; range.end += self.position; PartialVec::new(vec, range) } } impl AsyncMediaParser { pub fn new() -> Self { Self::default() } /// `MediaParser`/`AsyncMediaParser` comes with its own buffer management, /// so that buffers can be reused during multiple parsing processes to /// avoid frequent memory allocations. Therefore, try to reuse a /// `MediaParser` instead of creating a new one every time you need it. /// /// **Note**: /// /// - For [`ExifIter`] as parse output, Please avoid holding the `ExifIter` /// object all the time and drop it immediately after use. Otherwise, the /// parsing buffer referenced by the `ExifIter` object will not be reused /// by [`MediaParser`], resulting in repeated memory allocation in the /// subsequent parsing process. /// /// If you really need to retain some data, please take out the required /// Entry values โ€‹โ€‹and save them, or convert the `ExifIter` into an /// [`crate::Exif`] object to retain all Entry values. /// /// - For [`TrackInfo`] as parse output, you don't need to worry about /// this, because `TrackInfo` dosn't reference the parsing buffer. pub async fn parse>( &mut self, mut ms: AsyncMediaSource, ) -> crate::Result { self.reset(); self.acquire_buf(); self.buf_mut().append(&mut ms.buf); let res = self.do_parse(ms).await; self.reset(); res } async fn do_parse>( &mut self, mut ms: AsyncMediaSource, ) -> Result { self.fill_buf(&mut ms.reader, INIT_BUF_SIZE).await?; let res = O::parse(self, ms).await?; Ok(res) } fn reset(&mut self) { // Ensure buf has been released if let Some(buf) = self.buf.take() { self.bb.release(buf); } // Reset position self.set_position(0); } fn buf(&self) -> &Vec { self.buf.as_ref().unwrap() } fn acquire_buf(&mut self) { assert!(self.buf.is_none()); self.buf = Some(self.bb.acquire()); } fn buf_mut(&mut self) -> &mut Vec { self.buf.as_mut().unwrap() } } impl AsyncBufParser for AsyncMediaParser { #[tracing::instrument(skip(self, reader))] async fn fill_buf( &mut self, reader: &mut R, size: usize, ) -> io::Result { if size > MAX_ALLOC_SIZE { tracing::error!(?size, "the requested buffer size is too big"); return Err(io::ErrorKind::Unsupported.into()); } self.buf_mut().reserve_exact(size); let n = reader.take(size as u64).read_to_end(self.buf_mut()).await?; if n == 0 { return Err(std::io::ErrorKind::UnexpectedEof.into()); } // let n = reader.read_buf(&mut self.buf).await?; // if n == 0 { // return Err(std::io::ErrorKind::UnexpectedEof.into()); // } Ok(n) } } impl Buf for AsyncMediaParser { fn buffer(&self) -> &[u8] { &self.buf()[self.position()..] } fn clear(&mut self) { self.buf_mut().clear(); } fn set_position(&mut self, pos: usize) { self.position = pos; } fn position(&self) -> usize { self.position } } #[cfg(test)] mod tests { use std::path::Path; use super::*; use test_case::case; enum TrackExif { Track, Exif, NoData, Invalid, } use tokio::fs::File; use TrackExif::*; #[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[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("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)] async fn parse_media(path: &str, te: TrackExif) { let mut parser = AsyncMediaParser::new(); let ms = AsyncMediaSource::file_path(Path::new("testdata").join(path)).await; match te { Track => { let ms = ms.unwrap(); // println!("path: {path} mime: {:?}", ms.mime); assert!(ms.has_track()); let _: TrackInfo = parser.parse(ms).await.unwrap(); } Exif => { let ms = ms.unwrap(); // println!("path: {path} mime: {:?}", ms.mime); assert!(ms.has_exif()); let mut it: ExifIter = parser.parse(ms).await.unwrap(); let _ = it.parse_gps_info(); if path.contains("one-entry") { assert!(it.next().is_some()); assert!(it.next().is_none()); let exif: crate::Exif = it.clone_and_rewind().into(); assert!(exif.get(ExifTag::Orientation).is_some()); } else { let _: crate::Exif = it.clone_and_rewind().into(); } } NoData => { let ms = ms.unwrap(); // println!("path: {path} mime: {:?}", ms.mime); if ms.has_exif() { let res: Result = parser.parse(ms).await; res.unwrap_err(); } else if ms.has_track() { let res: Result = parser.parse(ms).await; res.unwrap_err(); } } Invalid => { ms.unwrap_err(); } } } use crate::{EntryValue, ExifTag, TrackInfoTag}; use chrono::DateTime; use test_case::test_case; use crate::video::TrackInfoTag::*; #[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[test_case("mkv_640x360.mkv", ImageWidth, 640_u32.into())] #[test_case("mkv_640x360.mkv", ImageHeight, 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.mp4", ImageWidth, 1920_u32.into())] #[test_case("meta.mp4", ImageHeight, 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())] async fn parse_track_info(path: &str, tag: TrackInfoTag, v: EntryValue) { let mut parser = AsyncMediaParser::new(); let f = File::open(Path::new("testdata").join(path)).await.unwrap(); let ms = AsyncMediaSource::file(f).await.unwrap(); let info: TrackInfo = parser.parse(ms).await.unwrap(); assert_eq!(info.get(tag).unwrap(), &v); let f = File::open(Path::new("testdata").join(path)).await.unwrap(); let ms = AsyncMediaSource::unseekable(f).await.unwrap(); let info: TrackInfo = parser.parse(ms).await.unwrap(); assert_eq!(info.get(tag).unwrap(), &v); } } nom-exif-2.5.4/src/partial_vec.rs000064400000000000000000000076171046102023000150170ustar 00000000000000use crate::slice::SubsliceRange as _; use std::borrow::Borrow; use std::fmt::Debug; use std::ops::Deref; use std::ops::Range; use std::sync::Arc; #[derive(Clone, PartialEq, Eq, Default)] pub(crate) struct PartialVec { pub(crate) data: Arc>, pub(crate) range: Range, } impl Debug for PartialVec { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("PartialVec") .field("data len", &self.data.len()) .field("range", &self.range) .finish() } } impl PartialVec { pub(crate) fn new(vec: Arc>, range: Range) -> PartialVec { assert!(range.end <= vec.len()); PartialVec { data: vec, range } } pub(crate) fn from_vec(vec: Vec) -> PartialVec { let range = 0..vec.len(); Self::from_vec_range(vec, range) } // pub(crate) fn to_vec(&self) -> Vec { // Vec::from(self.data.clone()) // } pub(crate) fn from_arc_vec_slice(vec: Arc>, subslice: &[u8]) -> PartialVec { let range = vec .subslice_in_range(subslice) .expect("subslice should be a sub slice of self"); Self::new(vec, range) } pub(crate) fn from_vec_range(vec: Vec, range: Range) -> PartialVec { assert!(range.end <= vec.len()); Self::new(Arc::new(vec), range) } pub(crate) fn partial(&self, subslice: &[u8]) -> AssociatedInput { let range = self .data .subslice_in_range(subslice) .expect("subslice should be a sub slice of self"); AssociatedInput::new(self.data.clone(), range) } } // impl<'a> From<&'a [u8]> for Input<'a> { // fn from(data: &'a [u8]) -> Self { // Input { // data: Cow::Borrowed(data), // range: Range { // start: 0, // end: data.len(), // }, // } // } // } impl From> for PartialVec { fn from(value: Vec) -> Self { PartialVec::from_vec(value) } } impl From<(Arc>, &[u8])> for PartialVec { fn from(value: (Arc>, &[u8])) -> Self { Self::from_arc_vec_slice(value.0, value.1) } } impl From<(Arc>, Range)> for PartialVec { fn from(value: (Arc>, Range)) -> Self { Self::new(value.0, value.1) } } impl From<(Vec, Range)> for PartialVec { fn from(value: (Vec, Range)) -> Self { let (data, range) = value; PartialVec::from_vec_range(data, range) } } impl Deref for PartialVec { type Target = [u8]; fn deref(&self) -> &Self::Target { &self.data[self.range.clone()] } } impl AsRef<[u8]> for PartialVec { fn as_ref(&self) -> &[u8] { &self.data[self.range.clone()] } } impl Borrow<[u8]> for PartialVec { fn borrow(&self) -> &[u8] { &self.data[self.range.clone()] } } pub(crate) type AssociatedInput = PartialVec; // #[derive(Clone, Debug, PartialEq, Eq)] // pub struct AssociatedInput { // data: Arc>, // range: Range, // // pub(crate) ptr: *const u8, // // pub(crate) len: usize, // } // // Since we only use `AssociatedInput` in Exif, it's safe to impl `Send` & // // `Sync` here. // unsafe impl Send for AssociatedInput {} // unsafe impl Sync for AssociatedInput {} // impl AssociatedInput { // pub(crate) fn make_associated(&self, subslice: &[u8]) -> AssociatedInput { // let _ = self // .subslice_in_range(subslice) // .expect("subslice should be a sub slice of self"); // AssociatedInput::new(subslice) // } // } // impl Deref for AssociatedInput { // type Target = [u8]; // fn deref(&self) -> &Self::Target { // &self.data[self.range.clone()] // } // } // impl AsRef<[u8]> for AssociatedInput { // fn as_ref(&self) -> &[u8] { // self // } // } nom-exif-2.5.4/src/raf.rs000064400000000000000000000064251046102023000132720ustar 00000000000000use nom::{ bytes::streaming::{tag, take}, number, IResult, }; use crate::{jpeg, utils::parse_cstr}; const MAGIC: &[u8] = b"FUJIFILMCCD-RAW "; /// Refer to: [Fujifilm RAF](http://fileformats.archiveteam.org/wiki/Fujifilm_RAF) #[allow(unused)] pub struct RafInfo<'a> { pub version: &'a [u8], pub camera_num_id: &'a [u8], pub camera_string: String, pub directory_ver: &'a [u8], pub image_offset: u32, pub exif_data: Option<&'a [u8]>, } impl RafInfo<'_> { pub fn check(input: &[u8]) -> crate::Result<()> { // check magic let _ = nom::bytes::complete::tag(MAGIC)(input)?; Ok(()) } pub(crate) fn parse(input: &[u8]) -> IResult<&[u8], RafInfo> { // magic let (remain, _) = tag(MAGIC)(input)?; let (remain, version) = take(4usize)(remain)?; let (remain, camera_num_id) = take(8usize)(remain)?; let (remain, camera_string) = take(32usize)(remain)?; let (remain, directory_ver) = take(4usize)(remain)?; // 20 bytes unknown let (remain, _) = take(20usize)(remain)?; let (remain, image_offset) = number::streaming::be_u32(remain)?; // skip to image_offset let skip_n = image_offset .checked_sub((input.len() - remain.len()) as u32) .ok_or_else(|| { nom::Err::Failure(nom::error::make_error(remain, nom::error::ErrorKind::Fail)) })?; let (remain, _) = take(skip_n)(remain)?; // parse as a JPEG jpeg::check_jpeg(remain).map_err(|_| { nom::Err::Failure(nom::error::make_error(remain, nom::error::ErrorKind::Fail)) })?; let (remain, exif_data) = jpeg::extract_exif_data(remain)?; let (_, camera_string) = parse_cstr(camera_string)?; Ok(( remain, RafInfo { version, camera_num_id, camera_string, directory_ver, image_offset, exif_data, }, )) } } #[cfg(test)] mod tests { use std::{fs::File, io::Write, path::Path}; use test_case::case; use crate::testkit::read_sample; use super::*; #[case("fujifilm_x_t1_01.raf.meta")] fn test_check_raf(path: &str) { let data = read_sample(path).unwrap(); RafInfo::check(&data).unwrap(); } // #[case("fujifilm_x_t1_01.raf", b"0201", b"FF119503", "X-T1", 0x94)] #[case("fujifilm_x_t1_01.raf.meta", b"0201", b"FF119503", "X-T1", 0x94)] fn test_extract_exif( path: &str, version: &[u8], camera_num_id: &[u8], camera_string: &str, image_offset: u32, ) { let data = read_sample(path).unwrap(); let (remain, raf) = RafInfo::parse(&data).unwrap(); assert_eq!(raf.version, version); assert_eq!(raf.camera_num_id, camera_num_id); assert_eq!(raf.camera_string, camera_string); assert_eq!(raf.image_offset, image_offset); raf.exif_data.unwrap(); // save header + exif_data let p = Path::new("./testdata").join("fujifilm_x_t1_01.raf.meta"); if !p.exists() { let size = data.len() - remain.len(); let mut f = File::create(p).unwrap(); f.write_all(&data[..size]).unwrap(); } } } nom-exif-2.5.4/src/skip.rs000064400000000000000000000131061046102023000134620ustar 00000000000000use std::{ fmt::Debug, io::{self, BufRead, Read, Seek}, }; #[cfg(feature = "async")] use tokio::io::{AsyncRead, AsyncSeek, AsyncSeekExt}; /// Seekable represents a *seek-able* `Read`, e.g. a `File`. /// /// Use `Seekable` as a generic parameter to tell the parser to use `Seek` to /// implement [`Skip`] operations. For more information, please refer to: /// [`parse_track_info`](crate::parse_track_info). #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Copy)] pub struct Seekable(()); /// Use `SkipRead` as a generic parameter for some interfaces, so tell the /// parser to use `Read` to implement [`Skip`] operations. For more /// information, please refer to: /// [`parse_track_info`](crate::parse_track_info). #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Copy)] pub struct Unseekable(()); #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Copy)] pub struct SkipBufRead(()); /// Abstracts the operation of skipping some bytes. /// /// The user specifies the parser's `Skip` behavior using [`SkipSeek`] or /// [`SkipRead`]. /// /// For more information, please refer to: /// [`parse_track_info`](crate::parse_track_info). #[allow(unused)] pub trait Skip { /// Skip the given number of bytes. fn skip(reader: &mut R, skip: u64) -> io::Result<()>; /// Skip the given number of bytes. If seek is not implemented by `reader`, /// `false` will be returned. /// /// Therefore, the caller can implement the skip function by himself, /// thereby reusing the caller's own buffer. fn skip_by_seek(reader: &mut R, skip: u64) -> io::Result; fn debug() -> impl Debug; } #[cfg(feature = "async")] pub trait AsyncSkip { /// Skip the given number of bytes. If seek is not implemented by `reader`, /// `false` will be returned. /// /// Therefore, the caller can implement the skip function by himself, /// thereby reusing the caller's own buffer. fn skip_by_seek( reader: &mut R, skip: u64, ) -> impl std::future::Future> + Send; fn debug() -> impl Debug; } impl Skip for Unseekable { #[inline] fn skip(reader: &mut R, skip: u64) -> io::Result<()> { // println!("unseekable..."); match std::io::copy(&mut reader.by_ref().take(skip), &mut std::io::sink()) { Ok(x) => { if x == skip { Ok(()) } else { Err(std::io::ErrorKind::UnexpectedEof.into()) } } Err(e) => Err(e), } } #[inline] fn skip_by_seek(_: &mut R, _: u64) -> io::Result { Ok(false) } fn debug() -> impl Debug { "unseekable" } } impl Skip for Seekable { #[inline] fn skip(reader: &mut R, skip: u64) -> io::Result<()> { // println!("seekable..."); reader.seek_relative(skip.try_into().unwrap()) } #[inline] fn skip_by_seek(reader: &mut R, skip: u64) -> io::Result { reader.seek_relative( skip.try_into() .map_err(|_| io::Error::from(io::ErrorKind::InvalidInput))?, )?; Ok(true) } fn debug() -> impl Debug { "seekable" } } #[cfg(feature = "async")] impl AsyncSkip for Unseekable { #[inline] async fn skip_by_seek(_: &mut R, _: u64) -> io::Result { Ok(false) } fn debug() -> impl Debug { "async unseekable" } } #[cfg(feature = "async")] impl AsyncSkip for Seekable { #[inline] async fn skip_by_seek(reader: &mut R, skip: u64) -> io::Result { match reader.seek(std::io::SeekFrom::Current(skip as i64)).await { Ok(_) => Ok(true), Err(e) => Err(e), } } fn debug() -> impl Debug { "async seekable" } } impl Skip for SkipBufRead { fn skip(reader: &mut R, mut skip: u64) -> io::Result<()> { while skip > 0 { let buffer = reader.fill_buf()?; if buffer.is_empty() { return Err(io::ErrorKind::UnexpectedEof.into()); } let consume = u64::try_from(buffer.len()).expect("should fit").min(skip); reader.consume(usize::try_from(consume).expect("must fit")); skip -= consume; } Ok(()) } fn skip_by_seek(_: &mut R, _: u64) -> io::Result { Ok(false) } fn debug() -> impl Debug { "unseekable(BufRead)" } } #[cfg(test)] mod tests { use super::*; use io::{repeat, Cursor}; fn parse, R: Read>(reader: &mut R) -> io::Result { S::skip_by_seek(reader, 2) } #[cfg(feature = "async")] async fn parse_async, R: AsyncRead + Unpin>( reader: &mut R, ) -> io::Result { S::skip_by_seek(reader, 2).await } #[test] fn skip() { let mut buf = Cursor::new([0u8, 3]); assert!(!parse::(&mut buf).unwrap()); assert!(parse::(&mut buf).unwrap()); let mut r = repeat(0); assert!(!parse::(&mut r).unwrap()); } #[cfg(feature = "async")] #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn skip_async() { let mut buf = Cursor::new([0u8, 3]); assert!(!parse_async::(&mut buf).await.unwrap()); assert!(parse_async::(&mut buf).await.unwrap()); let mut r = tokio::io::repeat(1); assert!(!parse_async::(&mut r).await.unwrap()); } } nom-exif-2.5.4/src/slice.rs000064400000000000000000000036661046102023000136250ustar 00000000000000use std::ops::Range; pub trait SliceChecked { fn slice_checked(&self, range: Range) -> Option<&Self>; } impl SliceChecked for [T] { fn slice_checked(&self, range: Range) -> Option<&Self> { if range.end <= self.len() { Some(&self[range]) } else { None } } } pub trait SubsliceOffset { fn subslice_offset(&self, inner: &Self) -> Option; } pub trait SubsliceRange { fn subslice_in_range(&self, inner: &Self) -> Option>; } impl SubsliceOffset for [T] { fn subslice_offset(&self, inner: &Self) -> Option { let start = self.as_ptr() as usize; let inner_start = inner.as_ptr() as usize; if inner_start < start || inner_start > start.wrapping_add(self.len()) { None } else { inner_start.checked_sub(start) } } } impl SubsliceRange for [T] where [T]: SubsliceOffset, { fn subslice_in_range(&self, inner: &Self) -> Option> { let offset = self.subslice_offset(inner)?; let end = offset.checked_add(inner.len())?; let start = self.as_ptr() as usize; if end > start + self.len() { None } else { Some(Range { start: offset, end }) } } } #[cfg(test)] mod tests { use super::SubsliceOffset; #[test] fn subslice_offset() { let a = &[0u8]; let v: Vec = vec![0, 1, 2, 3, 4, 5]; let b = &[0u8]; assert_eq!(v.subslice_offset(&v).unwrap(), 0); assert_eq!(v.subslice_offset(&v[1..2]).unwrap(), 1); assert_eq!(v.subslice_offset(&v[1..]).unwrap(), 1); assert_eq!(v.subslice_offset(&v[2..]).unwrap(), 2); assert_eq!(v.subslice_offset(&v[3..]).unwrap(), 3); assert_eq!(v.subslice_offset(&v[5..]).unwrap(), 5); assert!(v.subslice_offset(a).is_none()); assert!(v.subslice_offset(b).is_none()); } } nom-exif-2.5.4/src/testkit.rs000064400000000000000000000075641046102023000142160ustar 00000000000000use std::{fs::File, io::Read, path::Path}; use crate::exif::Exif; use crate::exif::ExifTag::*; pub fn read_sample(path: &str) -> Result, std::io::Error> { let mut f = open_sample(path)?; let mut buf = Vec::new(); f.read_to_end(&mut buf)?; Ok(buf) } pub fn open_sample(path: &str) -> Result { let p = Path::new(path); let p = if p.is_absolute() { p.to_path_buf() } else { Path::new("./testdata").join(p) }; File::open(p) } #[allow(unused)] pub fn open_sample_w(path: &str) -> Result { let p = Path::new(path); let p = if p.is_absolute() { p.to_path_buf() } else { Path::new("./testdata").join(p) }; File::create(p) } #[allow(deprecated)] pub fn sorted_exif_entries(exif: &Exif) -> Vec { let mut entries = exif .get_values(&[ Make, Model, Orientation, ImageWidth, ImageHeight, ISOSpeedRatings, ShutterSpeedValue, ExposureTime, FNumber, ExifImageWidth, ExifImageHeight, DateTimeOriginal, CreateDate, ModifyDate, OffsetTimeOriginal, OffsetTime, GPSLatitudeRef, GPSLatitude, GPSLongitudeRef, GPSLongitude, GPSAltitudeRef, GPSAltitude, GPSVersionID, // sub ifd ExifOffset, GPSInfo, ImageDescription, XResolution, YResolution, ResolutionUnit, Software, HostComputer, WhitePoint, PrimaryChromaticities, YCbCrCoefficients, ReferenceBlackWhite, Copyright, ExposureProgram, SpectralSensitivity, OECF, SensitivityType, ExifVersion, ApertureValue, BrightnessValue, ExposureBiasValue, MaxApertureValue, SubjectDistance, MeteringMode, LightSource, Flash, FocalLength, SubjectArea, MakerNote, // UserComment, FlashPixVersion, ColorSpace, RelatedSoundFile, FlashEnergy, FocalPlaneXResolution, FocalPlaneYResolution, FocalPlaneResolutionUnit, SubjectLocation, ExposureIndex, SensingMethod, FileSource, SceneType, CFAPattern, CustomRendered, ExposureMode, WhiteBalanceMode, DigitalZoomRatio, FocalLengthIn35mmFilm, SceneCaptureType, GainControl, Contrast, Saturation, Sharpness, DeviceSettingDescription, SubjectDistanceRange, ImageUniqueID, LensSpecification, LensMake, LensModel, Gamma, GPSTimeStamp, GPSSatellites, GPSStatus, GPSMeasureMode, GPSDOP, GPSSpeedRef, GPSSpeed, GPSTrackRef, GPSTrack, GPSImgDirectionRef, GPSImgDirection, GPSMapDatum, GPSDestLatitudeRef, GPSDestLatitude, GPSDestLongitudeRef, GPSDestLongitude, GPSDestBearingRef, GPSDestBearing, GPSDestDistanceRef, GPSDestDistance, GPSProcessingMethod, GPSAreaInformation, GPSDateStamp, GPSDifferential, ]) .into_iter() .map(|x| format!("{} ยป {}", x.0, x.1)) .collect::>(); entries.sort(); entries } nom-exif-2.5.4/src/utils.rs000064400000000000000000000016561046102023000136630ustar 00000000000000use nom::{combinator::map_res, IResult}; pub(crate) fn parse_cstr(input: &[u8]) -> IResult<&[u8], String> { let (remain, s) = map_res( nom::bytes::streaming::take_till(|b| b == 0), |bs: &[u8]| { if bs.is_empty() { Ok("".to_owned()) } else { String::from_utf8(bs.to_vec()) } }, )(input)?; // consumes the zero byte Ok((&remain[1..], s)) // Safe-slice } #[cfg(test)] mod tests { use super::*; use test_case::case; #[case(b"", None)] #[case(b"\0", Some(""))] #[case(b"h\0", Some("h"))] #[case(b"hello\0", Some("hello"))] #[case(b"hello", None)] fn test_check_raf(data: &[u8], expect: Option<&str>) { let res = parse_cstr(data); match expect { Some(s) => assert_eq!(res.unwrap().1, s), None => { res.unwrap_err(); } } } } nom-exif-2.5.4/src/values.rs000064400000000000000000000717621046102023000140270ustar 00000000000000use std::{ fmt::{Display, LowerHex}, string::FromUtf8Error, }; use chrono::{DateTime, FixedOffset, NaiveDateTime, Offset, Utc}; use nom::{multi::many_m_n, number::Endianness, AsChar}; #[cfg(feature = "json_dump")] use serde::{Deserialize, Serialize, Serializer}; use thiserror::Error; use crate::ExifTag; /// Represent a parsed entry value. #[derive(Debug, Clone, PartialEq)] #[non_exhaustive] pub enum EntryValue { Text(String), URational(URational), IRational(IRational), U8(u8), U16(u16), U32(u32), U64(u64), I8(i8), I16(i16), I32(i32), I64(i64), F32(f32), F64(f64), Time(DateTime), NaiveDateTime(NaiveDateTime), Undefined(Vec), URationalArray(Vec), IRationalArray(Vec), U8Array(Vec), U16Array(Vec), U32Array(Vec), } #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) struct EntryData<'a> { pub endian: Endianness, pub tag: u16, pub data: &'a [u8], pub data_format: DataFormat, pub components_num: u32, } #[derive(Debug, Clone, Error)] pub(crate) enum ParseEntryError { #[error("size is too big")] EntrySizeTooBig, #[error("data is invalid: {0}")] InvalidData(String), #[error("data format is unsupported (please file a bug): {0}")] Unsupported(String), } impl From for ParseEntryError { fn from(value: chrono::ParseError) -> Self { ParseEntryError::InvalidData(format!("invalid time format: {value}")) } } use ParseEntryError as Error; impl EntryData<'_> { // Ensure that the returned Vec is not empty. fn try_as_rationals(&self) -> Result>, Error> { if self.components_num == 0 { return Err(Error::InvalidData("components is 0".to_string())); } let mut vec = Vec::with_capacity(self.components_num as usize); for i in 0..self.components_num { let rational = decode_rational::(&self.data[i as usize * 8..], self.endian)?; vec.push(rational); } Ok(vec) } } impl EntryValue { /// Parse an IFD entry value. /// /// # Structure of IFD Entry /// /// ```txt /// | 2 | 2 | 4 | 4 | /// | tag | data format | components num | data (value or offset) | /// ``` /// /// # Data size /// /// `data_size = components_num * bytes_per_component` /// /// `bytes_per_component` is determined by tag & data format. /// /// If data_size > 4, then the data area of entry stores the offset of the /// value, not the value itself. /// /// # Data format /// /// See: [`DataFormat`]. pub(crate) fn parse(entry: &EntryData, tz: &Option) -> Result { if entry.data.is_empty() { return Err(Error::InvalidData( "invalid DirectoryEntry: entry data is empty".into(), )); } let endian = entry.endian; let tag = entry.tag; let data_format = entry.data_format; let data = entry.data; let components_num = entry.components_num; if data.is_empty() || components_num == 0 { return Ok(EntryValue::variant_default(data_format)); } let exif_tag: Result = tag.try_into(); if let Ok(tag) = exif_tag { if tag == ExifTag::DateTimeOriginal || tag == ExifTag::CreateDate || tag == ExifTag::ModifyDate { // assert_eq!(data_format, 2); // if data_format != 2 { // return Err(Error::InvalidData( // "invalid DirectoryEntry: date format is invalid".into(), // )); // } let s = get_cstr(data).map_err(|e| Error::InvalidData(e.to_string()))?; let t = if let Some(tz) = tz { let tz = repair_tz_str(tz); let ss = format!("{s} {tz}"); match DateTime::parse_from_str(&ss, "%Y:%m:%d %H:%M:%S %z") { Ok(t) => t, Err(_) => return Ok(EntryValue::NaiveDateTime(parse_naive_time(s)?)), } } else { return Ok(EntryValue::NaiveDateTime(parse_naive_time(s)?)); }; return Ok(EntryValue::Time(t)); } } match data_format { DataFormat::U8 => match components_num { 1 => Ok(Self::U8(data[0])), _ => Ok(Self::U8Array(data.into())), }, DataFormat::Text => Ok(EntryValue::Text( get_cstr(data).map_err(|e| Error::InvalidData(e.to_string()))?, )), DataFormat::U16 => { if components_num == 1 { Ok(Self::U16(u16::try_from_bytes(data, endian)?)) } else { let (_, v) = many_m_n::<_, _, nom::error::Error<_>, _>( components_num as usize, components_num as usize, nom::number::complete::u16(endian), )(data) .map_err(|e| { ParseEntryError::InvalidData(format!("parse U16Array error: {e:?}")) })?; Ok(Self::U16Array(v)) } } DataFormat::U32 => { if components_num == 1 { Ok(Self::U32(u32::try_from_bytes(data, endian)?)) } else { let (_, v) = many_m_n::<_, _, nom::error::Error<_>, _>( components_num as usize, components_num as usize, nom::number::complete::u32(endian), )(data) .map_err(|e| { ParseEntryError::InvalidData(format!("parse U32Array error: {e:?}")) })?; Ok(Self::U32Array(v)) } } DataFormat::URational => { let rationals = entry.try_as_rationals::()?; if rationals.len() == 1 { Ok(Self::URational(rationals[0])) } else { Ok(Self::URationalArray(rationals)) } } DataFormat::I8 => match components_num { 1 => Ok(Self::I8(data[0] as i8)), x => Err(Error::Unsupported(format!( "signed byte with {x} components" ))), }, DataFormat::Undefined => Ok(Self::Undefined(data.to_vec())), DataFormat::I16 => match components_num { 1 => Ok(Self::I16(i16::try_from_bytes(data, endian)?)), x => Err(Error::Unsupported(format!( "signed short with {x} components" ))), }, DataFormat::I32 => match components_num { 1 => Ok(Self::I32(i32::try_from_bytes(data, endian)?)), x => Err(Error::Unsupported(format!( "signed long with {x} components" ))), }, DataFormat::IRational => { let rationals = entry.try_as_rationals::()?; if rationals.len() == 1 { Ok(Self::IRational(rationals[0])) } else { Ok(Self::IRationalArray(rationals)) } } DataFormat::F32 => match components_num { 1 => Ok(Self::F32(f32::try_from_bytes(data, endian)?)), x => Err(Error::Unsupported(format!("float with {x} components"))), }, DataFormat::F64 => match components_num { 1 => Ok(Self::F64(f64::try_from_bytes(data, endian)?)), x => Err(Error::Unsupported(format!("double with {x} components"))), }, } } fn variant_default(data_format: DataFormat) -> EntryValue { match data_format { DataFormat::U8 => Self::U8(0), DataFormat::Text => Self::Text(String::default()), DataFormat::U16 => Self::U16(0), DataFormat::U32 => Self::U32(0), DataFormat::URational => Self::URational(URational::default()), DataFormat::I8 => Self::I8(0), DataFormat::Undefined => Self::Undefined(Vec::default()), DataFormat::I16 => Self::I16(0), DataFormat::I32 => Self::I32(0), DataFormat::IRational => Self::IRational(IRational::default()), DataFormat::F32 => Self::F32(0.0), DataFormat::F64 => Self::F64(0.0), } } pub fn as_str(&self) -> Option<&str> { match self { EntryValue::Text(v) => Some(v), _ => None, } } pub fn as_time(&self) -> Option> { match self { EntryValue::Time(v) => Some(*v), _ => None, } } pub fn as_u8(&self) -> Option { match self { EntryValue::U8(v) => Some(*v), _ => None, } } pub fn as_i8(&self) -> Option { match self { EntryValue::I8(v) => Some(*v), _ => None, } } pub fn as_u16(&self) -> Option { match self { EntryValue::U16(v) => Some(*v), _ => None, } } pub fn as_i16(&self) -> Option { match self { EntryValue::I16(v) => Some(*v), _ => None, } } pub fn as_u64(&self) -> Option { match self { EntryValue::U64(v) => Some(*v), _ => None, } } pub fn as_u32(&self) -> Option { match self { EntryValue::U32(v) => Some(*v), _ => None, } } pub fn as_i32(&self) -> Option { match self { EntryValue::I32(v) => Some(*v), _ => None, } } pub fn as_urational(&self) -> Option { if let EntryValue::URational(v) = self { Some(*v) } else { None } } pub fn as_irational(&self) -> Option { if let EntryValue::IRational(v) = self { Some(*v) } else { None } } pub fn as_urational_array(&self) -> Option<&[URational]> { if let EntryValue::URationalArray(v) = self { Some(v) } else { None } } pub fn as_irational_array(&self) -> Option<&[IRational]> { if let EntryValue::IRationalArray(v) = self { Some(v) } else { None } } pub fn as_u8array(&self) -> Option<&[u8]> { if let EntryValue::U8Array(v) = self { Some(v) } else { None } } pub fn to_u8array(self) -> Option> { if let EntryValue::U8Array(v) = self { Some(v) } else { None } } } fn parse_naive_time(s: String) -> Result { let t = NaiveDateTime::parse_from_str(&s, "%Y:%m:%d %H:%M:%S")?; Ok(t) } // fn parse_time_with_local_tz(s: String) -> Result, ParseEntryError> { // let t = NaiveDateTime::parse_from_str(&s, "%Y:%m:%d %H:%M:%S")?; // let t = Local.from_local_datetime(&t); // let t = if let LocalResult::Single(t) = t { // Ok(t) // } else { // Err(Error::InvalidData(format!("parse time failed: {s}"))) // }?; // Ok(t.with_timezone(t.offset())) // } fn repair_tz_str(tz: &str) -> String { if let Some(idx) = tz.find(":") { if tz[idx..].len() < 3 { // Add tailed 0 return format!("{tz}0"); } } tz.into() } /// # Exif Data format /// /// ```txt /// | Value | 1 | 2 | 3 | 4 | 5 | 6 | /// |-----------------+---------------+---------------+----------------+-----------------+-------------------+--------------| /// | Format | unsigned byte | ascii strings | unsigned short | unsigned long | unsigned rational | signed byte | /// | Bytes/component | 1 | 1 | 2 | 4 | 8 | 1 | /// /// | Value | 7 | 8 | 9 | 10 | 11 | 12 | /// |-----------------+---------------+---------------+----------------+-----------------+-------------------+--------------| /// | Format | undefined | signed short | signed long | signed rational | single float | double float | /// | Bytes/component | 1 | 2 | 4 | 8 | 4 | 8 | /// ``` /// /// See: [Exif](https://www.media.mit.edu/pia/Research/deepview/exif.html). #[repr(u16)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[allow(unused)] pub(crate) enum DataFormat { U8 = 1, Text = 2, U16 = 3, U32 = 4, URational = 5, I8 = 6, Undefined = 7, I16 = 8, I32 = 9, IRational = 10, F32 = 11, F64 = 12, } impl DataFormat { pub fn component_size(&self) -> usize { match self { Self::U8 | Self::I8 | Self::Text | Self::Undefined => 1, Self::U16 | Self::I16 => 2, Self::U32 | Self::I32 | Self::F32 => 4, Self::URational | Self::IRational | Self::F64 => 8, } } } impl TryFrom for DataFormat { type Error = Error; fn try_from(v: u16) -> Result { if v >= Self::U8 as u16 && v <= Self::F64 as u16 { Ok(unsafe { std::mem::transmute::(v) }) } else { Err(Error::InvalidData(format!("data format 0x{v:02x}"))) } } } #[cfg(feature = "json_dump")] impl Serialize for EntryValue { fn serialize(&self, serializer: S) -> Result where S: Serializer, { serializer.serialize_str(&self.to_string()) } } // impl std::fmt::Debug for EntryValue { // fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { // Display::fmt(self, f) // } // } impl Display for EntryValue { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { EntryValue::Text(v) => v.fmt(f), EntryValue::URational(v) => { format!("{}/{} ({:.04})", v.0, v.1, v.0 as f64 / v.1 as f64).fmt(f) } EntryValue::IRational(v) => { format!("{}/{} ({:.04})", v.0, v.1, v.0 as f64 / v.1 as f64).fmt(f) } EntryValue::U32(v) => Display::fmt(&v, f), EntryValue::U16(v) => Display::fmt(&v, f), EntryValue::U64(v) => Display::fmt(&v, f), EntryValue::I16(v) => Display::fmt(&v, f), EntryValue::I32(v) => Display::fmt(&v, f), EntryValue::I64(v) => Display::fmt(&v, f), EntryValue::F32(v) => Display::fmt(&v, f), EntryValue::F64(v) => Display::fmt(&v, f), EntryValue::U8(v) => Display::fmt(&v, f), EntryValue::I8(v) => Display::fmt(&v, f), EntryValue::Time(v) => Display::fmt(&v.to_rfc3339(), f), EntryValue::NaiveDateTime(v) => Display::fmt(&v.format("%Y-%m-%d %H:%M:%S"), f), EntryValue::Undefined(v) => fmt_array_to_string("Undefined", v, f), EntryValue::URationalArray(v) => { format!("URationalArray[{}]", rationals_to_string::(v)).fmt(f) } EntryValue::IRationalArray(v) => { format!("IRationalArray[{}]", rationals_to_string::(v)).fmt(f) } EntryValue::U8Array(v) => fmt_array_to_string("U8Array", v, f), EntryValue::U32Array(v) => fmt_array_to_string("U32Array", v, f), EntryValue::U16Array(v) => fmt_array_to_string("U16Array", v, f), } } } pub(crate) fn fmt_array_to_string( name: &str, v: &[T], f: &mut std::fmt::Formatter, ) -> Result<(), std::fmt::Error> { array_to_string(name, v).fmt(f) // format!( // "{}[{}]", // name, // v.iter() // .map(|x| x.to_string()) // .collect::>() // .join(", ") // ) // .fmt(f) } pub(crate) fn array_to_string(name: &str, v: &[T]) -> String { // Display up to MAX_DISPLAY_NUM components, and replace the rest with ellipsis const MAX_DISPLAY_NUM: usize = 8; let s = v .iter() .map(|x| format!("0x{x:02x}")) .take(MAX_DISPLAY_NUM + 1) .enumerate() .map(|(i, x)| { if i >= MAX_DISPLAY_NUM { "...".to_owned() } else { x } }) .collect::>() .join(", "); format!("{}[{}]", name, s) } fn rationals_to_string(rationals: &[Rational]) -> String where T: Display + Into + Copy, { // Display up to MAX_DISPLAY_NUM components, and replace the rest with ellipsis const MAX_DISPLAY_NUM: usize = 3; rationals .iter() .map(|x| format!("{}/{} ({:.04})", x.0, x.1, x.0.into() / x.1.into())) .take(MAX_DISPLAY_NUM + 1) .enumerate() .map(|(i, x)| { if i >= MAX_DISPLAY_NUM { "...".to_owned() } else { x } }) .collect::>() .join(", ") } impl From> for EntryValue { fn from(value: DateTime) -> Self { assert_eq!(value.offset().fix(), FixedOffset::east_opt(0).unwrap()); EntryValue::Time(value.fixed_offset()) } } impl From> for EntryValue { fn from(value: DateTime) -> Self { EntryValue::Time(value) } } impl From for EntryValue { fn from(value: u8) -> Self { EntryValue::U8(value) } } impl From for EntryValue { fn from(value: u16) -> Self { EntryValue::U16(value) } } impl From for EntryValue { fn from(value: u32) -> Self { EntryValue::U32(value) } } impl From for EntryValue { fn from(value: u64) -> Self { EntryValue::U64(value) } } impl From for EntryValue { fn from(value: i8) -> Self { EntryValue::I8(value) } } impl From for EntryValue { fn from(value: i16) -> Self { EntryValue::I16(value) } } impl From for EntryValue { fn from(value: i32) -> Self { EntryValue::I32(value) } } impl From for EntryValue { fn from(value: i64) -> Self { EntryValue::I64(value) } } impl From for EntryValue { fn from(value: f32) -> Self { EntryValue::F32(value) } } impl From for EntryValue { fn from(value: f64) -> Self { EntryValue::F64(value) } } impl From for EntryValue { fn from(value: String) -> Self { EntryValue::Text(value) } } impl From<&String> for EntryValue { fn from(value: &String) -> Self { EntryValue::Text(value.to_owned()) } } impl From<&str> for EntryValue { fn from(value: &str) -> Self { value.to_owned().into() } } impl From<(u32, u32)> for EntryValue { fn from(value: (u32, u32)) -> Self { Self::URational(value.into()) } } impl From<(i32, i32)> for EntryValue { fn from(value: (i32, i32)) -> Self { Self::IRational((value.0, value.1).into()) } } // #[cfg_attr(feature = "json_dump", derive(Serialize, Deserialize))] // #[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] // pub struct URational(pub u32, pub u32); pub type URational = Rational; pub type IRational = Rational; #[cfg_attr(feature = "json_dump", derive(Serialize, Deserialize))] #[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] pub struct Rational(pub T, pub T); impl Rational where T: Copy + Into, { pub fn as_float(&self) -> f64 { std::convert::Into::::into(self.0) / std::convert::Into::::into(self.1) } } impl From<(T, T)> for Rational where T: Copy, { fn from(value: (T, T)) -> Self { Self(value.0, value.1) } } impl From> for (T, T) where T: Copy, { fn from(value: Rational) -> Self { (value.0, value.1) } } impl From for URational { fn from(value: IRational) -> Self { Self(value.0 as u32, value.1 as u32) } } pub(crate) fn get_cstr(data: &[u8]) -> std::result::Result { let vec = filter_zero(data); if let Ok(s) = String::from_utf8(vec) { Ok(s) } else { Ok(filter_zero(data) .into_iter() .map(|x| x.as_char()) .collect::()) } } pub(crate) fn filter_zero(data: &[u8]) -> Vec { data.iter() // skip leading zero bytes .skip_while(|b| **b == 0) // ignore tailing zero bytes, and all bytes after zero bytes .take_while(|b| **b != 0) .cloned() .collect::>() } pub(crate) trait TryFromBytes: Sized { fn try_from_bytes(bs: &[u8], endian: Endianness) -> Result; } impl TryFromBytes for u32 { fn try_from_bytes(bs: &[u8], endian: Endianness) -> Result { fn make_err() -> Error { Error::InvalidData(format!( "data is too small to convert to {}", std::any::type_name::(), )) } match endian { Endianness::Big => { let (int_bytes, _) = bs .split_at_checked(std::mem::size_of::()) .ok_or_else(make_err::)?; Ok(Self::from_be_bytes( int_bytes.try_into().map_err(|_| make_err::())?, )) } Endianness::Little => { let (int_bytes, _) = bs .split_at_checked(std::mem::size_of::()) .ok_or_else(make_err::)?; Ok(Self::from_le_bytes( int_bytes.try_into().map_err(|_| make_err::())?, )) } Endianness::Native => unimplemented!(), } } } impl TryFromBytes for i32 { fn try_from_bytes(bs: &[u8], endian: Endianness) -> Result { fn make_err() -> Error { Error::InvalidData(format!( "data is too small to convert to {}", std::any::type_name::(), )) } match endian { Endianness::Big => { let (int_bytes, _) = bs .split_at_checked(std::mem::size_of::()) .ok_or_else(make_err::)?; Ok(Self::from_be_bytes( int_bytes.try_into().map_err(|_| make_err::())?, )) } Endianness::Little => { let (int_bytes, _) = bs .split_at_checked(std::mem::size_of::()) .ok_or_else(make_err::)?; Ok(Self::from_le_bytes( int_bytes.try_into().map_err(|_| make_err::())?, )) } Endianness::Native => unimplemented!(), } } } impl TryFromBytes for u16 { fn try_from_bytes(bs: &[u8], endian: Endianness) -> Result { fn make_err() -> Error { Error::InvalidData(format!( "data is too small to convert to {}", std::any::type_name::(), )) } match endian { Endianness::Big => { let (int_bytes, _) = bs .split_at_checked(std::mem::size_of::()) .ok_or_else(make_err::)?; Ok(Self::from_be_bytes( int_bytes.try_into().map_err(|_| make_err::())?, )) } Endianness::Little => { let (int_bytes, _) = bs .split_at_checked(std::mem::size_of::()) .ok_or_else(make_err::)?; Ok(Self::from_le_bytes( int_bytes.try_into().map_err(|_| make_err::())?, )) } Endianness::Native => unimplemented!(), } } } impl TryFromBytes for i16 { fn try_from_bytes(bs: &[u8], endian: Endianness) -> Result { fn make_err() -> Error { Error::InvalidData(format!( "data is too small to convert to {}", std::any::type_name::(), )) } match endian { Endianness::Big => { let (int_bytes, _) = bs .split_at_checked(std::mem::size_of::()) .ok_or_else(make_err::)?; Ok(Self::from_be_bytes( int_bytes.try_into().map_err(|_| make_err::())?, )) } Endianness::Little => { let (int_bytes, _) = bs .split_at_checked(std::mem::size_of::()) .ok_or_else(make_err::)?; Ok(Self::from_le_bytes( int_bytes.try_into().map_err(|_| make_err::())?, )) } Endianness::Native => unimplemented!(), } } } impl TryFromBytes for f32 { fn try_from_bytes(bs: &[u8], endian: Endianness) -> Result { fn make_err() -> Error { Error::InvalidData(format!( "data is too small to convert to {}", std::any::type_name::(), )) } match endian { Endianness::Big => { let (int_bytes, _) = bs .split_at_checked(std::mem::size_of::()) .ok_or_else(make_err::)?; Ok(Self::from_be_bytes( int_bytes.try_into().map_err(|_| make_err::())?, )) } Endianness::Little => { let (int_bytes, _) = bs .split_at_checked(std::mem::size_of::()) .ok_or_else(make_err::)?; Ok(Self::from_le_bytes( int_bytes.try_into().map_err(|_| make_err::())?, )) } Endianness::Native => unimplemented!(), } } } impl TryFromBytes for f64 { fn try_from_bytes(bs: &[u8], endian: Endianness) -> Result { fn make_err() -> Error { Error::InvalidData(format!( "data is too small to convert to {}", std::any::type_name::(), )) } match endian { Endianness::Big => { let (int_bytes, _) = bs .split_at_checked(std::mem::size_of::()) .ok_or_else(make_err::)?; Ok(Self::from_be_bytes( int_bytes.try_into().map_err(|_| make_err::())?, )) } Endianness::Little => { let (int_bytes, _) = bs .split_at_checked(std::mem::size_of::()) .ok_or_else(make_err::)?; Ok(Self::from_le_bytes( int_bytes.try_into().map_err(|_| make_err::())?, )) } Endianness::Native => unimplemented!(), } } } pub(crate) fn decode_rational( data: &[u8], endian: Endianness, ) -> Result, Error> { if data.len() < 8 { return Err(Error::InvalidData( "data is too small to decode a rational".to_string(), )); } let numerator = T::try_from_bytes(data, endian)?; let denominator = T::try_from_bytes(&data[4..], endian)?; // Safe-slice Ok(Rational::(numerator, denominator)) } #[cfg(test)] mod tests { use chrono::{Local, NaiveDateTime, TimeZone}; use super::*; #[test] fn test_parse_time() { let tz = Local::now().format("%:z").to_string(); let s = format!("2023:07:09 20:36:33 {tz}"); let t1 = DateTime::parse_from_str(&s, "%Y:%m:%d %H:%M:%S %z").unwrap(); let s = "2023:07:09 20:36:33"; let t2 = NaiveDateTime::parse_from_str(s, "%Y:%m:%d %H:%M:%S").unwrap(); let t2 = Local.from_local_datetime(&t2).unwrap(); let t3 = t2.with_timezone(t2.offset()); assert_eq!(t1, t2); assert_eq!(t1, t3); } #[test] fn test_iso_8601() { let s = "2023-11-02T19:58:34+0800"; let t1 = DateTime::parse_from_str(s, "%+").unwrap(); let s = "2023-11-02T19:58:34+08:00"; let t2 = DateTime::parse_from_str(s, "%+").unwrap(); let s = "2023-11-02T19:58:34.026490+08:00"; let t3 = DateTime::parse_from_str(s, "%+").unwrap(); assert_eq!(t1, t2); assert!(t3 > t2); } } nom-exif-2.5.4/src/video.rs000064400000000000000000000166461046102023000136360ustar 00000000000000use std::{ collections::{btree_map::IntoIter, BTreeMap}, fmt::Display, }; use thiserror::Error; use crate::{ ebml::webm::parse_webm, error::ParsingError, file::MimeVideo, mov::{extract_moov_body_from_buf, parse_mp4, parse_qt}, EntryValue, GPSInfo, }; /// Try to keep the tag name consistent with [`crate::ExifTag`], and add some /// unique to video/audio, such as `DurationMs`. /// /// Different variants of `TrackInfoTag` may have different value types, please /// refer to the documentation of each variant. #[derive(Debug, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, Hash)] #[non_exhaustive] pub enum TrackInfoTag { /// Its value is an `EntryValue::Text`. Make, /// Its value is an `EntryValue::Text`. Model, /// Its value is an `EntryValue::Text`. Software, /// Its value is an [`EntryValue::Time`]. CreateDate, /// Duration in millisecond, its value is an `EntryValue::U64`. DurationMs, /// Its value is an `EntryValue::U32`. ImageWidth, /// Its value is an `EntryValue::U32`. ImageHeight, /// Its value is an `EntryValue::Text`, location presented in ISO6709. /// /// If you need a parsed [`GPSInfo`] which provides more detailed GPS info, /// please use [`TrackInfo::get_gps_info`]. GpsIso6709, /// Its value is an `EntryValue::Text`. Author, } /// Represents parsed track info. #[derive(Debug, Clone, Default)] pub struct TrackInfo { entries: BTreeMap, gps_info: Option, } impl TrackInfo { /// Get value for `tag`. Different variants of `TrackInfoTag` may have /// different value types, please refer to [`TrackInfoTag`]. pub fn get(&self, tag: TrackInfoTag) -> Option<&EntryValue> { self.entries.get(&tag) } /// Get parsed `GPSInfo`. pub fn get_gps_info(&self) -> Option<&GPSInfo> { self.gps_info.as_ref() } /// Get an iterator for `(&TrackInfoTag, &EntryValue)`. The parsed /// `GPSInfo` is not included. pub fn iter(&self) -> impl Iterator { self.entries.iter() } pub(crate) fn put(&mut self, tag: TrackInfoTag, value: EntryValue) { self.entries.insert(tag, value); } } /// Parse video/audio info from `reader`. The file format will be detected /// automatically by parser, if the format is not supported, an `Err` will be /// returned. /// /// Currently supported file formats are: /// /// - ISO base media file format (ISOBMFF): *.mp4, *.mov, *.3gp, etc. /// - Matroska based file format: *.webm, *.mkv, *.mka, etc. /// /// ## Explanation of the generic parameters of this function: /// /// - In order to improve parsing efficiency, the parser will internally skip /// some useless bytes during parsing the byte stream, which is called /// [`Skip`] internally. /// /// - In order to support both `Read` and `Read` + `Seek` types, the interface /// of input parameters is defined as `Read`. /// /// - Since Rust does not support specialization, the parser cannot internally /// distinguish between `Read` and `Seek` and provide different `Skip` /// implementations for them. /// /// Therefore, We chose to let the user specify how `Skip` works: /// /// - `parse_track_info::(reader)` means the `reader` supports /// `Seek`, so `Skip` will use the `Seek` trait to implement efficient skip /// operations. /// /// - `parse_track_info::(reader)` means the `reader` dosn't /// support `Seek`, so `Skip` will fall back to using `Read` to implement the /// skip operations. /// /// ## Performance impact /// /// If your `reader` only supports `Read`, it may cause performance loss when /// processing certain large files. For example, *.mov files place metadata at /// the end of the file, therefore, when parsing such files, locating metadata /// will be slightly slower. /// /// ## Examples /// /// ```rust /// use nom_exif::*; /// use std::fs::File; /// use chrono::DateTime; /// /// let ms = MediaSource::file_path("./testdata/meta.mov").unwrap(); /// let mut parser = MediaParser::new(); /// let info: TrackInfo = parser.parse(ms).unwrap(); /// /// assert_eq!(info.get(TrackInfoTag::Make), Some(&"Apple".into())); /// assert_eq!(info.get(TrackInfoTag::Model), Some(&"iPhone X".into())); /// assert_eq!(info.get(TrackInfoTag::GpsIso6709), Some(&"+27.1281+100.2508+000.000/".into())); /// assert_eq!(info.get_gps_info().unwrap().latitude_ref, 'N'); /// assert_eq!( /// info.get_gps_info().unwrap().latitude, /// [(27, 1), (7, 1), (68, 100)].into(), /// ); /// ``` #[tracing::instrument(skip(input))] pub(crate) fn parse_track_info( input: &[u8], mime_video: MimeVideo, ) -> Result { let mut info: TrackInfo = match mime_video { crate::file::MimeVideo::QuickTime | crate::file::MimeVideo::_3gpp | crate::file::MimeVideo::Mp4 => { let range = extract_moov_body_from_buf(input)?; let moov_body = &input[range]; match mime_video { MimeVideo::QuickTime => parse_qt(moov_body)?.into(), MimeVideo::Mp4 | MimeVideo::_3gpp => parse_mp4(moov_body)?.into(), _ => unreachable!(), } } crate::file::MimeVideo::Webm | crate::file::MimeVideo::Matroska => { parse_webm(input)?.into() } }; if let Some(gps) = info.get(TrackInfoTag::GpsIso6709) { info.gps_info = gps.as_str().and_then(|s| s.parse().ok()); } Ok(info) } impl IntoIterator for TrackInfo { type Item = (TrackInfoTag, EntryValue); type IntoIter = IntoIter; fn into_iter(self) -> Self::IntoIter { self.entries.into_iter() } } impl From> for TrackInfo { fn from(entries: BTreeMap) -> Self { Self { entries, gps_info: None, } } } impl Display for TrackInfoTag { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let s: &str = (*self).into(); s.fmt(f) } } impl From for &str { fn from(value: TrackInfoTag) -> Self { match value { TrackInfoTag::Make => "Make", TrackInfoTag::Model => "Model", TrackInfoTag::Software => "Software", TrackInfoTag::CreateDate => "CreateDate", TrackInfoTag::DurationMs => "DurationMs", TrackInfoTag::ImageWidth => "ImageWidth", TrackInfoTag::ImageHeight => "ImageHeight", TrackInfoTag::GpsIso6709 => "GpsIso6709", TrackInfoTag::Author => "Author", } } } #[derive(Debug, Error)] #[error("unknown TrackInfoTag: {0}")] pub struct UnknownTrackInfoTag(pub String); impl TryFrom<&str> for TrackInfoTag { type Error = UnknownTrackInfoTag; fn try_from(value: &str) -> Result { let tag = match value { "Make" => TrackInfoTag::Make, "Model" => TrackInfoTag::Model, "Software" => TrackInfoTag::Software, "CreateDate" => TrackInfoTag::CreateDate, "DurationMs" => TrackInfoTag::DurationMs, "ImageWidth" => TrackInfoTag::ImageWidth, "ImageHeight" => TrackInfoTag::ImageHeight, "GpsIso6709" => TrackInfoTag::GpsIso6709, "Author" => TrackInfoTag::Author, x => return Err(UnknownTrackInfoTag(x.to_owned())), }; Ok(tag) } }