oxedyne/fe2o3/fe2o3_file/tests/exif.rs
20.8 KiB, 26 runs
created by r1870400018:17744, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\ |
| 2 | //! Anthropic Claude |
| 3 | |
| 4 | use oxedyne_fe2o3_file::exif::{ |
| 5 | ByteOrder, |
| 6 | Exif, |
| 7 | IfdKind, |
| 8 | PhotoMeta, |
| 9 | Rational, |
| 10 | Value, |
| 11 | tag, |
| 12 | }; |
| 13 | |
| 14 | use oxedyne_fe2o3_core::{ |
| 15 | prelude::*, |
| 16 | test::test_it, |
| 17 | }; |
| 18 | |
| 19 | use std::{ |
| 20 | fs, |
| 21 | path::PathBuf, |
| 22 | }; |
| 23 | |
| 24 | |
| 25 | fn u16b(o: ByteOrder, v: u16) -> [u8; 2] { |
| 26 | match o { |
| 27 | ByteOrder::Little => v.to_le_bytes(), |
| 28 | ByteOrder::Big => v.to_be_bytes(), |
| 29 | } |
| 30 | } |
| 31 | |
| 32 | fn u32b(o: ByteOrder, v: u32) -> [u8; 4] { |
| 33 | match o { |
| 34 | ByteOrder::Little => v.to_le_bytes(), |
| 35 | ByteOrder::Big => v.to_be_bytes(), |
| 36 | } |
| 37 | } |
| 38 | |
| 39 | /// A run of unsigned ratios, in the given order. |
| 40 | fn rats(o: ByteOrder, pairs: &[(u32, u32)]) -> Vec<u8> { |
| 41 | let mut v = Vec::new(); |
| 42 | for (n, d) in pairs { |
| 43 | v.extend_from_slice(&u32b(o, *n)); |
| 44 | v.extend_from_slice(&u32b(o, *d)); |
| 45 | } |
| 46 | v |
| 47 | } |
| 48 | |
| 49 | /// One entry of a hand-built IFD, its payload already in the target byte order. |
| 50 | struct Entry { |
| 51 | tag: u16, |
| 52 | typ: u16, |
| 53 | count: u32, |
| 54 | dat: Vec<u8>, |
| 55 | } |
| 56 | |
| 57 | impl Entry { |
| 58 | |
| 59 | /// NUL terminated, as the standard requires. |
| 60 | fn ascii(tag: u16, s: &str) -> Self { |
| 61 | let mut dat = s.as_bytes().to_vec(); |
| 62 | dat.push(0); |
| 63 | let count = dat.len() as u32; |
| 64 | Self { tag, typ: 2, count, dat } |
| 65 | } |
| 66 | |
| 67 | fn short(o: ByteOrder, tag: u16, v: u16) -> Self { |
| 68 | Self { tag, typ: 3, count: 1, dat: u16b(o, v).to_vec() } |
| 69 | } |
| 70 | |
| 71 | fn long(o: ByteOrder, tag: u16, v: u32) -> Self { |
| 72 | Self { tag, typ: 4, count: 1, dat: u32b(o, v).to_vec() } |
| 73 | } |
| 74 | |
| 75 | fn byte(tag: u16, v: u8) -> Self { |
| 76 | Self { tag, typ: 1, count: 1, dat: vec![v] } |
| 77 | } |
| 78 | |
| 79 | fn rational(o: ByteOrder, tag: u16, pairs: &[(u32, u32)]) -> Self { |
| 80 | Self { tag, typ: 5, count: pairs.len() as u32, dat: rats(o, pairs) } |
| 81 | } |
| 82 | |
| 83 | /// A type code this library does not recognise. |
| 84 | fn odd_type(tag: u16, typ: u16, count: u32, raw: [u8; 4]) -> Self { |
| 85 | Self { tag, typ, count, dat: raw.to_vec() } |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | fn ifd_len(n: usize) -> usize { |
| 90 | 2 + 12 * n + 4 |
| 91 | } |
| 92 | |
| 93 | /// Oversized payloads are appended to `heap`, which begins at `hoff`. |
| 94 | fn write_ifd( |
| 95 | o: ByteOrder, |
| 96 | ents: &[Entry], |
| 97 | next: u32, |
| 98 | heap: &mut Vec<u8>, |
| 99 | hoff: usize, |
| 100 | ) |
| 101 | -> Vec<u8> |
| 102 | { |
| 103 | let mut out = Vec::new(); |
| 104 | out.extend_from_slice(&u16b(o, ents.len() as u16)); |
| 105 | for e in ents { |
| 106 | out.extend_from_slice(&u16b(o, e.tag)); |
| 107 | out.extend_from_slice(&u16b(o, e.typ)); |
| 108 | out.extend_from_slice(&u32b(o, e.count)); |
| 109 | if e.dat.len() <= 4 { |
| 110 | let mut inline = [0u8; 4]; |
| 111 | inline[..e.dat.len()].copy_from_slice(&e.dat); |
| 112 | out.extend_from_slice(&inline); |
| 113 | } else { |
| 114 | let at = hoff + heap.len(); |
| 115 | out.extend_from_slice(&u32b(o, at as u32)); |
| 116 | heap.extend_from_slice(&e.dat); |
| 117 | if heap.len() % 2 == 1 { |
| 118 | heap.push(0); // Keep payloads on even boundaries, as cameras do. |
| 119 | } |
| 120 | } |
| 121 | } |
| 122 | out.extend_from_slice(&u32b(o, next)); |
| 123 | out |
| 124 | } |
| 125 | |
| 126 | /// Builds a complete TIFF block carrying a known set of values in the given byte order. |
| 127 | /// |
| 128 | /// The layout is IFD0, then the EXIF sub-IFD, the GPS sub-IFD and IFD1, then a heap of the |
| 129 | /// payloads too large to sit inside an entry. |
| 130 | fn build_fixture(o: ByteOrder) -> Vec<u8> { |
| 131 | |
| 132 | let n0 = 6usize; // IFD0 entry count, including the two sub-IFD pointers |
| 133 | let ne = 12usize; // EXIF sub-IFD entry count |
| 134 | let ng = 7usize; // GPS sub-IFD entry count |
| 135 | let n1 = 2usize; // IFD1 entry count |
| 136 | |
| 137 | let off0 = 8usize; |
| 138 | let offe = off0 + ifd_len(n0); |
| 139 | let offg = offe + ifd_len(ne); |
| 140 | let off1 = offg + ifd_len(ng); |
| 141 | let hoff = off1 + ifd_len(n1); |
| 142 | |
| 143 | let ifd0 = vec![ |
| 144 | Entry::ascii(tag::MAKE, "Oxide Optics"), |
| 145 | Entry::ascii(tag::MODEL, "Model 7 Field"), |
| 146 | Entry::short(o, tag::ORIENTATION, 6), |
| 147 | Entry::ascii(tag::DATE_TIME, "2019:04:07 13:45:09"), |
| 148 | Entry::long(o, tag::EXIF_IFD_POINTER, offe as u32), |
| 149 | Entry::long(o, tag::GPS_IFD_POINTER, offg as u32), |
| 150 | ]; |
| 151 | let exif = vec![ |
| 152 | Entry::rational(o, tag::EXPOSURE_TIME, &[(1, 250)]), |
| 153 | Entry::rational(o, tag::F_NUMBER, &[(28, 10)]), |
| 154 | Entry::short(o, tag::ISO_SPEED_RATINGS, 400), |
| 155 | Entry::ascii(tag::DATE_TIME_ORIGINAL, "2019:04:07 13:45:02"), |
| 156 | Entry::ascii(tag::CREATE_DATE, "2019:04:07 13:45:03"), |
| 157 | Entry::ascii(tag::SUBSEC_TIME_ORIGINAL, "880"), |
| 158 | Entry::ascii(tag::OFFSET_TIME_ORIGINAL, "+08:00"), |
| 159 | Entry::rational(o, tag::FOCAL_LENGTH, &[(350, 10)]), |
| 160 | Entry::long(o, tag::EXIF_IMAGE_WIDTH, 4032), |
| 161 | Entry::long(o, tag::EXIF_IMAGE_HEIGHT, 3024), |
| 162 | // A tag this library has no name for, which must survive rather than be dropped. |
| 163 | Entry::short(o, 0xBEEF, 7), |
| 164 | // A type code this library has no decoder for, likewise. |
| 165 | Entry::odd_type(0xBEEE, 199, 4, [0xDE, 0xAD, 0xBE, 0xEF]), |
| 166 | ]; |
| 167 | let gps = vec![ |
| 168 | Entry::ascii(tag::GPS_LATITUDE_REF, "S"), |
| 169 | Entry::rational(o, tag::GPS_LATITUDE, &[(31, 1), (57, 1), (1234, 100)]), |
| 170 | Entry::ascii(tag::GPS_LONGITUDE_REF, "E"), |
| 171 | Entry::rational(o, tag::GPS_LONGITUDE, &[(115, 1), (51, 1), (5678, 100)]), |
| 172 | Entry::byte(tag::GPS_ALTITUDE_REF, 1), |
| 173 | Entry::rational(o, tag::GPS_ALTITUDE, &[(4567, 100)]), |
| 174 | Entry::ascii(tag::GPS_DATESTAMP, "2019:04:07"), |
| 175 | ]; |
| 176 | let ifd1 = vec![ |
| 177 | Entry::long(o, tag::JPEG_INTERCHANGE_FORMAT, 900), |
| 178 | Entry::long(o, tag::JPEG_INTERCHANGE_LENGTH, 128), |
| 179 | ]; |
| 180 | |
| 181 | let mut heap = Vec::new(); |
| 182 | let b0 = write_ifd(o, &ifd0, off1 as u32, &mut heap, hoff); |
| 183 | let be = write_ifd(o, &exif, 0, &mut heap, hoff); |
| 184 | let bg = write_ifd(o, &gps, 0, &mut heap, hoff); |
| 185 | let b1 = write_ifd(o, &ifd1, 0, &mut heap, hoff); |
| 186 | |
| 187 | let mut out = Vec::new(); |
| 188 | match o { |
| 189 | ByteOrder::Little => out.extend_from_slice(b"II"), |
| 190 | ByteOrder::Big => out.extend_from_slice(b"MM"), |
| 191 | } |
| 192 | out.extend_from_slice(&u16b(o, 42)); |
| 193 | out.extend_from_slice(&u32b(o, off0 as u32)); |
| 194 | out.extend_from_slice(&b0); |
| 195 | out.extend_from_slice(&be); |
| 196 | out.extend_from_slice(&bg); |
| 197 | out.extend_from_slice(&b1); |
| 198 | out.extend_from_slice(&heap); |
| 199 | out |
| 200 | } |
| 201 | |
| 202 | /// Names the field on failure. |
| 203 | fn near(what: &str, got: Option<f64>, want: f64, tol: f64) -> Outcome<()> { |
| 204 | match got { |
| 205 | Some(g) if (g - want).abs() <= tol => Ok(()), |
| 206 | Some(g) => Err(err!( |
| 207 | "{}: parsed {}, expected {} to within {}.", what, g, want, tol; |
| 208 | Test, Mismatch)), |
| 209 | None => Err(err!( |
| 210 | "{}: no value parsed, expected {}.", what, want; |
| 211 | Test, Missing)), |
| 212 | } |
| 213 | } |
| 214 | |
| 215 | fn check_fixture_meta(m: &PhotoMeta) -> Outcome<()> { |
| 216 | req!(m.make.as_deref(), Some("Oxide Optics")); |
| 217 | req!(m.model.as_deref(), Some("Model 7 Field")); |
| 218 | req!(m.orientation, Some(6u16)); |
| 219 | req!(m.datetime_original.as_deref(), Some("2019:04:07 13:45:02")); |
| 220 | req!(m.create_date.as_deref(), Some("2019:04:07 13:45:03")); |
| 221 | req!(m.modify_date.as_deref(), Some("2019:04:07 13:45:09")); |
| 222 | req!(m.subsec_time_original.as_deref(), Some("880")); |
| 223 | req!(m.offset_time_original.as_deref(), Some("+08:00")); |
| 224 | req!(m.width, Some(4032u32)); |
| 225 | req!(m.height, Some(3024u32)); |
| 226 | req!(m.iso, Some(400u32)); |
| 227 | req!(m.gps_datestamp.as_deref(), Some("2019:04:07")); |
| 228 | res!(near("FNumber", m.f_number, 2.8, 1e-9)); |
| 229 | res!(near("ExposureTime", m.exposure_time, 0.004, 1e-9)); |
| 230 | res!(near("FocalLength", m.focal_length, 35.0, 1e-9)); |
| 231 | // 31 degrees 57 minutes 12.34 seconds south. |
| 232 | res!(near("GPSLatitude", m.gps_latitude, -31.953427777777, 1e-9)); |
| 233 | // 115 degrees 51 minutes 56.78 seconds east. |
| 234 | res!(near("GPSLongitude", m.gps_longitude, 115.865772222222, 1e-9)); |
| 235 | res!(near("GPSAltitude", m.gps_altitude, -45.67, 1e-9)); |
| 236 | Ok(()) |
| 237 | } |
| 238 | |
| 239 | fn fixture_path(name: &str) -> PathBuf { |
| 240 | let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); |
| 241 | p.push("test_images"); |
| 242 | p.push(name); |
| 243 | p |
| 244 | } |
| 245 | |
| 246 | pub fn test_exif(filter: &'static str) -> Outcome<()> { |
| 247 | |
| 248 | res!(test_it(filter, &["Synthetic TIFF, both byte orders 000", "all", "exif"], || { |
| 249 | for o in [ByteOrder::Little, ByteOrder::Big] { |
| 250 | let dat = build_fixture(o); |
| 251 | let exif = res!(Exif::from_tiff(&dat)); |
| 252 | req!(exif.order, o); |
| 253 | let m = exif.meta(); |
| 254 | res!(check_fixture_meta(&m)); |
| 255 | |
| 256 | // The thumbnail pointer pair from IFD1. |
| 257 | match exif.thumbnail { |
| 258 | Some((off, len)) => { |
| 259 | req!(off, 900u32); |
| 260 | req!(len, 128u32); |
| 261 | }, |
| 262 | None => return Err(err!( |
| 263 | "{}: IFD1 thumbnail offset and length were not recovered.", o; |
| 264 | Test, Missing)), |
| 265 | } |
| 266 | |
| 267 | // Every IFD must have been reached. |
| 268 | for (kind, n) in [ |
| 269 | (IfdKind::Ifd0, 6usize), |
| 270 | (IfdKind::Exif, 12), |
| 271 | (IfdKind::Gps, 7), |
| 272 | (IfdKind::Ifd1, 2), |
| 273 | ] { |
| 274 | req!(exif.ifd(kind).len(), n, "in {}", kind); |
| 275 | } |
| 276 | } |
| 277 | Ok(()) |
| 278 | })); |
| 279 | |
| 280 | res!(test_it(filter, &["Unknown tags and types survive 010", "all", "exif"], || { |
| 281 | for o in [ByteOrder::Little, ByteOrder::Big] { |
| 282 | let dat = build_fixture(o); |
| 283 | let exif = res!(Exif::from_tiff(&dat)); |
| 284 | |
| 285 | // An unnamed tag of a known type keeps its decoded value. |
| 286 | match exif.field(IfdKind::Exif, 0xBEEF) { |
| 287 | Some(f) => req!(f.value, Value::Short(vec![7])), |
| 288 | None => return Err(err!( |
| 289 | "{}: the unnamed tag 0xBEEF was dropped.", o; Test, Missing)), |
| 290 | } |
| 291 | // A tag of an unknown type keeps its raw four bytes. |
| 292 | match exif.field(IfdKind::Exif, 0xBEEE) { |
| 293 | Some(f) => req!( |
| 294 | f.value, |
| 295 | Value::Unknown { typ: 199, count: 4, raw: [0xDE, 0xAD, 0xBE, 0xEF] } |
| 296 | ), |
| 297 | None => return Err(err!( |
| 298 | "{}: the tag 0xBEEE of unknown type was dropped.", o; Test, Missing)), |
| 299 | } |
| 300 | } |
| 301 | Ok(()) |
| 302 | })); |
| 303 | |
| 304 | res!(test_it(filter, &["Ratios keep their exact terms 020", "all", "exif"], || { |
| 305 | let dat = build_fixture(ByteOrder::Big); |
| 306 | let exif = res!(Exif::from_tiff(&dat)); |
| 307 | match exif.field(IfdKind::Exif, tag::EXPOSURE_TIME) { |
| 308 | Some(f) => req!(f.value, Value::Rational(vec![Rational { num: 1, den: 250 }])), |
| 309 | None => return Err(err!("ExposureTime was not parsed."; Test, Missing)), |
| 310 | } |
| 311 | match exif.field(IfdKind::Gps, tag::GPS_LATITUDE) { |
| 312 | Some(f) => req!(f.value, Value::Rational(vec![ |
| 313 | Rational { num: 31, den: 1 }, |
| 314 | Rational { num: 57, den: 1 }, |
| 315 | Rational { num: 1234, den: 100 }, |
| 316 | ])), |
| 317 | None => return Err(err!("GPSLatitude was not parsed."; Test, Missing)), |
| 318 | } |
| 319 | Ok(()) |
| 320 | })); |
| 321 | |
| 322 | res!(test_it(filter, &["Truncation at every boundary 030", "all", "exif"], || { |
| 323 | // A parse of any prefix must return, never panic and never invent a value. Where a |
| 324 | // prefix does parse, the values it yields must agree with the whole. |
| 325 | let full = build_fixture(ByteOrder::Little); |
| 326 | let whole = res!(Exif::from_tiff(&full)).meta(); |
| 327 | let mut parsed = 0usize; |
| 328 | let mut refused = 0usize; |
| 329 | for n in 0..full.len() { |
| 330 | match Exif::from_tiff(&full[..n]) { |
| 331 | Ok(exif) => { |
| 332 | let m = exif.meta(); |
| 333 | // Any field a truncated block does produce must match the whole. |
| 334 | if m.make.is_some() { |
| 335 | req!(m.make, whole.make, "at truncation length {}", n); |
| 336 | } |
| 337 | if m.gps_latitude.is_some() { |
| 338 | res!(near("GPSLatitude", m.gps_latitude, -31.953427777777, 1e-9)); |
| 339 | } |
| 340 | parsed += 1; |
| 341 | }, |
| 342 | Err(_) => refused += 1, |
| 343 | } |
| 344 | } |
| 345 | req!(parsed + refused, full.len()); |
| 346 | // The header alone cannot yield a complete IFD, so most prefixes must be refused. |
| 347 | if refused == 0 { |
| 348 | return Err(err!( |
| 349 | "No truncated prefix of a {} byte block was refused, which cannot be right.", |
| 350 | full.len(); |
| 351 | Test, Invalid)); |
| 352 | } |
| 353 | test!("Truncation sweep: {} prefixes parsed, {} refused.", parsed, refused); |
| 354 | Ok(()) |
| 355 | })); |
| 356 | |
| 357 | res!(test_it(filter, &["Truncated JPEG at every boundary 040", "all", "exif"], || { |
| 358 | let dat = res!(fs::read(fixture_path("exif_MM.jpg")), IO, File); |
| 359 | let mut parsed = 0usize; |
| 360 | let mut refused = 0usize; |
| 361 | for n in 0..dat.len() { |
| 362 | match Exif::from_jpeg(&dat[..n]) { |
| 363 | Ok(_) => parsed += 1, |
| 364 | Err(_) => refused += 1, |
| 365 | } |
| 366 | } |
| 367 | req!(parsed + refused, dat.len()); |
| 368 | test!("JPEG truncation sweep: {} prefixes parsed, {} refused.", parsed, refused); |
| 369 | Ok(()) |
| 370 | })); |
| 371 | |
| 372 | res!(test_it(filter, &["A looping IFD pointer is refused 050", "all", "exif"], || { |
| 373 | let o = ByteOrder::Little; |
| 374 | let mut dat = build_fixture(o); |
| 375 | // IFD0 sits at offset 8; point its next-IFD field back at itself. |
| 376 | let n0 = o.u16([dat[8], dat[9]]) as usize; |
| 377 | let noff = 8 + 2 + n0 * 12; |
| 378 | dat[noff..noff + 4].copy_from_slice(&u32b(o, 8)); |
| 379 | match Exif::from_tiff(&dat) { |
| 380 | Ok(_) => Err(err!( |
| 381 | "A chain whose IFD0 points back at itself was accepted."; Test, Invalid)), |
| 382 | Err(e) => { |
| 383 | let s = fmt!("{}", e); |
| 384 | if !s.contains("loops") { |
| 385 | return Err(err!( |
| 386 | "The error for a looping chain did not name the loop: {}", s; |
| 387 | Test, Mismatch)); |
| 388 | } |
| 389 | test!("Looping chain refused: {}", s); |
| 390 | Ok(()) |
| 391 | }, |
| 392 | } |
| 393 | })); |
| 394 | |
| 395 | res!(test_it(filter, &["A sub-IFD pointing at IFD0 is refused 060", "all", "exif"], || { |
| 396 | let o = ByteOrder::Big; |
| 397 | let mut dat = build_fixture(o); |
| 398 | // The fifth IFD0 entry is the EXIF sub-IFD pointer; aim it at IFD0. |
| 399 | let voff = 8 + 2 + 4 * 12 + 8; |
| 400 | dat[voff..voff + 4].copy_from_slice(&u32b(o, 8)); |
| 401 | match Exif::from_tiff(&dat) { |
| 402 | Ok(_) => Err(err!( |
| 403 | "A sub-IFD pointer aimed at IFD0 was accepted."; Test, Invalid)), |
| 404 | Err(e) => { |
| 405 | test!("Self-referential sub-IFD refused: {}", e); |
| 406 | Ok(()) |
| 407 | }, |
| 408 | } |
| 409 | })); |
| 410 | |
| 411 | res!(test_it(filter, &["An overflowing entry count is refused 070", "all", "exif"], || { |
| 412 | let o = ByteOrder::Little; |
| 413 | let mut dat = build_fixture(o); |
| 414 | // The second EXIF entry is FNumber, a RATIONAL; claim four thousand million of them. |
| 415 | let offe = 8 + ifd_len(6); |
| 416 | let coff = offe + 2 + 12 + 4; |
| 417 | dat[coff..coff + 4].copy_from_slice(&u32b(o, 0xFFFF_FFFF)); |
| 418 | match Exif::from_tiff(&dat) { |
| 419 | Ok(_) => Err(err!( |
| 420 | "An entry claiming 4294967295 ratios was accepted."; Test, Invalid)), |
| 421 | Err(e) => { |
| 422 | let s = fmt!("{}", e); |
| 423 | if !s.contains("4294967295") { |
| 424 | return Err(err!( |
| 425 | "The error for an overflowing count did not name it: {}", s; |
| 426 | Test, Mismatch)); |
| 427 | } |
| 428 | test!("Overflowing count refused: {}", s); |
| 429 | Ok(()) |
| 430 | }, |
| 431 | } |
| 432 | })); |
| 433 | |
| 434 | res!(test_it(filter, &["An absurd IFD entry count is refused 080", "all", "exif"], || { |
| 435 | let o = ByteOrder::Little; |
| 436 | let mut dat = build_fixture(o); |
| 437 | dat[8..10].copy_from_slice(&u16b(o, 0xFFFF)); |
| 438 | match Exif::from_tiff(&dat) { |
| 439 | Ok(_) => Err(err!( |
| 440 | "An IFD declaring 65535 entries was accepted."; Test, Invalid)), |
| 441 | Err(e) => { |
| 442 | test!("Absurd entry count refused: {}", e); |
| 443 | Ok(()) |
| 444 | }, |
| 445 | } |
| 446 | })); |
| 447 | |
| 448 | res!(test_it(filter, &["A value offset past the block is refused 090", "all", "exif"], || { |
| 449 | let o = ByteOrder::Big; |
| 450 | let mut dat = build_fixture(o); |
| 451 | // The first IFD0 entry is Make, whose payload lives on the heap; move it out of range. |
| 452 | let voff = 8 + 2 + 8; |
| 453 | dat[voff..voff + 4].copy_from_slice(&u32b(o, 0x7FFF_0000)); |
| 454 | match Exif::from_tiff(&dat) { |
| 455 | Ok(_) => Err(err!( |
| 456 | "A value offset beyond the block was accepted."; Test, Invalid)), |
| 457 | Err(e) => { |
| 458 | let s = fmt!("{}", e); |
| 459 | if !s.contains("extends beyond") { |
| 460 | return Err(err!( |
| 461 | "The error for an out of range value offset was unclear: {}", s; |
| 462 | Test, Mismatch)); |
| 463 | } |
| 464 | test!("Out of range value offset refused: {}", s); |
| 465 | Ok(()) |
| 466 | }, |
| 467 | } |
| 468 | })); |
| 469 | |
| 470 | res!(test_it(filter, &["A bad header is refused 100", "all", "exif"], || { |
| 471 | // Neither II nor MM. |
| 472 | match Exif::from_tiff(b"XX\x00\x2a\x00\x00\x00\x08") { |
| 473 | Ok(_) => return Err(err!("A header with no byte order mark was accepted."; |
| 474 | Test, Invalid)), |
| 475 | Err(e) => test!("Bad byte order mark refused: {}", e), |
| 476 | } |
| 477 | // Wrong magic number. |
| 478 | match Exif::from_tiff(b"MM\x00\x2b\x00\x00\x00\x08") { |
| 479 | Ok(_) => return Err(err!("A header with magic 43 was accepted."; Test, Invalid)), |
| 480 | Err(e) => test!("Bad magic refused: {}", e), |
| 481 | } |
| 482 | // Too short for a header at all. |
| 483 | match Exif::from_tiff(b"MM\x00") { |
| 484 | Ok(_) => return Err(err!("A three byte header was accepted."; Test, Invalid)), |
| 485 | Err(e) => test!("Short header refused: {}", e), |
| 486 | } |
| 487 | // Not a JPEG and not a TIFF. |
| 488 | match Exif::from_bytes(b"\x89PNG\r\n\x1a\n") { |
| 489 | Ok(_) => return Err(err!("A PNG signature was accepted."; Test, Invalid)), |
| 490 | Err(e) => test!("Foreign signature refused: {}", e), |
| 491 | } |
| 492 | Ok(()) |
| 493 | })); |
| 494 | |
| 495 | res!(test_it(filter, &["A JPEG without EXIF yields None 110", "all", "exif"], || { |
| 496 | // Start of image, a comment segment, then end of image. |
| 497 | let dat = b"\xFF\xD8\xFF\xFE\x00\x05hi!\xFF\xD9"; |
| 498 | match res!(Exif::from_jpeg(dat)) { |
| 499 | Some(_) => Err(err!("EXIF was reported for a JPEG that has none."; Test, Invalid)), |
| 500 | None => Ok(()), |
| 501 | } |
| 502 | })); |
| 503 | |
| 504 | res!(test_it(filter, &["JPEG fixtures match the reference tool 120", "all", "exif"], || { |
| 505 | // The expected values below are those reported by exiftool 13.50 for these two files, |
| 506 | // which were written by that tool, one in each byte order. See test_images/README.txt. |
| 507 | for (name, order) in [ |
| 508 | ("exif_MM.jpg", ByteOrder::Big), |
| 509 | ("exif_II.jpg", ByteOrder::Little), |
| 510 | ] { |
| 511 | let dat = res!(fs::read(fixture_path(name)), IO, File); |
| 512 | let exif = match res!(Exif::from_jpeg(&dat)) { |
| 513 | Some(e) => e, |
| 514 | None => return Err(err!( |
| 515 | "{}: no EXIF APP1 segment was found.", name; Test, Missing)), |
| 516 | }; |
| 517 | req!(exif.order, order, "in {}", name); |
| 518 | let m = exif.meta(); |
| 519 | req!(m.make.as_deref(), Some("Oxide Optics"), "in {}", name); |
| 520 | req!(m.model.as_deref(), Some("Model 7 Field"), "in {}", name); |
| 521 | req!(m.orientation, Some(6u16), "in {}", name); |
| 522 | req!(m.datetime_original.as_deref(), Some("2019:04:07 13:45:02"), "in {}", name); |
| 523 | req!(m.create_date.as_deref(), Some("2019:04:07 13:45:03"), "in {}", name); |
| 524 | req!(m.subsec_time_original.as_deref(), Some("880"), "in {}", name); |
| 525 | req!(m.iso, Some(400u32), "in {}", name); |
| 526 | req!(m.width, Some(24u32), "in {}", name); |
| 527 | req!(m.height, Some(16u32), "in {}", name); |
| 528 | req!(m.lens_model.as_deref(), Some("35mm f/2 Prime"), "in {}", name); |
| 529 | res!(near("ExposureTime", m.exposure_time, 0.004, 1e-9)); |
| 530 | res!(near("FNumber", m.f_number, 2.8, 1e-9)); |
| 531 | res!(near("FocalLength", m.focal_length, 35.0, 1e-9)); |
| 532 | // exiftool prints the composite values -31.9534276999917, 115.865772200058 |
| 533 | // and -45.67 for these three. |
| 534 | res!(near("GPSLatitude", m.gps_latitude, -31.9534276999917, 1e-7)); |
| 535 | res!(near("GPSLongitude", m.gps_longitude, 115.865772200058, 1e-7)); |
| 536 | res!(near("GPSAltitude", m.gps_altitude, -45.67, 1e-6)); |
| 537 | |
| 538 | // The frame dimensions from the start of frame marker, which identify reports as |
| 539 | // Image Width 24 and Image Height 16. |
| 540 | match res!(Exif::jpeg_dimensions(&dat)) { |
| 541 | Some((w, h)) => { |
| 542 | req!(w, 24u32, "in {}", name); |
| 543 | req!(h, 16u32, "in {}", name); |
| 544 | }, |
| 545 | None => return Err(err!( |
| 546 | "{}: no start of frame marker was found.", name; Test, Missing)), |
| 547 | } |
| 548 | } |
| 549 | Ok(()) |
| 550 | })); |
| 551 | |
| 552 | Ok(()) |
| 553 | } |