Oregami
Repositories/oxedyne/fe2o3

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
4use oxedyne_fe2o3_file::exif::{
5 ByteOrder,
6 Exif,
7 IfdKind,
8 PhotoMeta,
9 Rational,
10 Value,
11 tag,
12};
13
14use oxedyne_fe2o3_core::{
15 prelude::*,
16 test::test_it,
17};
18
19use std::{
20 fs,
21 path::PathBuf,
22};
23
24
25fn 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
32fn 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.
40fn 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.
50struct Entry {
51 tag: u16,
52 typ: u16,
53 count: u32,
54 dat: Vec<u8>,
55}
56
57impl 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
89fn ifd_len(n: usize) -> usize {
90 2 + 12 * n + 4
91}
92
93/// Oversized payloads are appended to `heap`, which begins at `hoff`.
94fn 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.
130fn 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.
203fn 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
215fn 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
239fn 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
246pub 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}