From 5c619072de9fca0df25cf4ea0eebcb4b6004d8b0 Mon Sep 17 00:00:00 2001 From: swackhamer Date: Sun, 13 Sep 2026 09:27:53 -0500 Subject: [PATCH 01/12] feat(tables): admit authenticated serial subdirectory edges --- src/exiftool_tables/ifd_engine.rs | 6 +- src/exiftool_tables/ifd_schema.rs | 33 ++++++- src/exiftool_tables/mod.rs | 4 +- tools/exiftool-tables/codegen.py | 115 +++++++++++++++++++--- tools/exiftool-tables/test_codegen_ifd.py | 97 +++++++++++++++++- 5 files changed, 230 insertions(+), 25 deletions(-) diff --git a/src/exiftool_tables/ifd_engine.rs b/src/exiftool_tables/ifd_engine.rs index 62590d098..6939e7b07 100644 --- a/src/exiftool_tables/ifd_engine.rs +++ b/src/exiftool_tables/ifd_engine.rs @@ -150,7 +150,9 @@ use crate::io::ByteOrder; use super::cond::{self, MemberValue}; use super::engine::{self, Dir, Emitted, Guard}; use super::exprs; -use super::ifd_schema::{IfdByteOrder, IfdStart, IfdSubdirEdge, IfdTable, IfdTag, RawConvEffect}; +use super::ifd_schema::{ + IfdByteOrder, IfdStart, IfdSubdirEdge, IfdSubdirProcessor, IfdTable, IfdTag, RawConvEffect, +}; use super::runtime::{self, DecodedValue, decode_value_of}; use super::subdir::BaseExpr; use super::{Fmt, find_ifd_table, find_table}; @@ -1677,6 +1679,8 @@ mod tests { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, } } diff --git a/src/exiftool_tables/ifd_schema.rs b/src/exiftool_tables/ifd_schema.rs index 9f7cd5096..4cecbc60d 100644 --- a/src/exiftool_tables/ifd_schema.rs +++ b/src/exiftool_tables/ifd_schema.rs @@ -48,7 +48,7 @@ use super::cond::Cond; use super::subdir::BaseExpr; -use super::{ExprId, Fmt, GateA, Omitted, PrintConv, TagGroups}; +use super::{ExprId, Fmt, GateA, Omitted, PrintConv, TagGroups, U16SizeCheck}; /// One `ProcessExif` (IFD-style) tag table. #[derive(Clone, Copy, Debug)] @@ -244,6 +244,20 @@ pub enum IfdByteOrder { Unknown, } +/// The source-selected processor for an IFD `SubDirectory` target. +/// +/// `Native` preserves the existing target lookup: an IFD target is walked by +/// `ProcessExif` and a binary target by `ProcessBinaryData`. `Serial` is +/// emitted only when the target table's effective `PROCESS_PROC` (or an +/// explicitly equivalent `SubDirectory.ProcessProc`) passed the independently +/// captured `ProcessSerialData` descriptor. It is deliberately a processor +/// classification rather than a module/table dispatch key. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum IfdSubdirProcessor { + Native, + Serial, +} + /// One `SubDirectory` edge out of an IFD tag. #[derive(Clone, Copy, Debug)] pub struct IfdSubdirEdge { @@ -272,11 +286,20 @@ pub struct IfdSubdirEdge { /// `DirName`, when declared (the family-1 name the sub-directory reports /// under, e.g. `Olympus2`). pub dir_name: Option<&'static str>, - /// `Validate` declared: ExifTool evaluates Perl against the directory - /// bytes before walking it. Not compiled -- an edge that carries it is - /// emitted for the reachability census but never walked - /// (`ifd_subdir_refused_validate`). + /// `Validate` was declared in the native edge. This preserves the source + /// fact even when [`Self::validation`] is unavailable, so legacy IFD and + /// binary edges continue to withhold rather than silently acquiring a new + /// execution path. pub validate: bool, + /// A source-authenticated, independently audited size check. This is + /// populated only for a supported serial target; `None` alongside + /// `validate: true` keeps the edge unwalked. + pub validation: Option, + /// The effective native processor selected from the target table's + /// `PROCESS_PROC` unless the edge explicitly overrides it. The compiler + /// refuses an override that is not equivalent to the target's captured + /// serial processor. + pub processor: IfdSubdirProcessor, /// Emitted for the reachability census but never walked, and why /// (slice IFD1, `codegen.py::compile_ifd_subdir`): /// `"same-table recursion (TagTable absent)"` -- ExifTool walks the diff --git a/src/exiftool_tables/mod.rs b/src/exiftool_tables/mod.rs index ef1cd1bc5..65c271b3f 100644 --- a/src/exiftool_tables/mod.rs +++ b/src/exiftool_tables/mod.rs @@ -71,8 +71,8 @@ pub use ifd_engine::{ read_ifd, }; pub use ifd_schema::{ - IfdByteOrder, IfdFlags, IfdStart, IfdSubdirEdge, IfdTable, IfdTag, IfdVariantGroup, - RawConvEffect, + IfdByteOrder, IfdFlags, IfdStart, IfdSubdirEdge, IfdSubdirProcessor, IfdTable, IfdTag, + IfdVariantGroup, RawConvEffect, }; pub use ifd_tables::{ALL_IFD_TABLES, IFD_EXIFTOOL_VERSION}; pub use keyed_engine::{ diff --git a/tools/exiftool-tables/codegen.py b/tools/exiftool-tables/codegen.py index 206fcbb84..ee6f01da6 100644 --- a/tools/exiftool-tables/codegen.py +++ b/tools/exiftool-tables/codegen.py @@ -27,6 +27,7 @@ from collections import Counter import conds +import directory_validation import exprs import hooks import others @@ -2158,38 +2159,107 @@ class IfdGenContext: itself under the module's `arrays` (:285-313), so the two are equal lists of the same 94 rows -- an exact fact, not a name heuristic.""" - def __init__(self, ifd_tables, binary_tables, makernotes_main=None): + def __init__(self, ifd_tables, binary_tables, makernotes_main=None, + serial_tables=None, table_processors=None, + validation_helpers=None, reader_contracts=None): self.ifd_tables = ifd_tables self.binary_tables = binary_tables self.makernotes_main = makernotes_main + # `(module, table) -> compiled serial processor facts`. This is a + # source-derived admission result, never a hand-maintained route list. + self.serial_tables = serial_tables or {} + # Retain the target table's actual `PROCESS_PROC` so an explicit edge + # override can be compared with it before the reader ever sees bytes. + self.table_processors = table_processors or {} + self.validation_helpers = validation_helpers or {} + self.reader_contracts = reader_contracts or {} @classmethod def from_doc(cls, doc): - ifd, binary = set(), set() + ifd, binary, serial, processors = set(), set(), {}, {} for mod_name, mod in doc["modules"].items(): for tbl_name, tbl in mod.get("tables", {}).items(): meta = tbl.get("meta") or {} + processors[(mod_name, tbl_name)] = meta.get("PROCESS_PROC") if is_binary_table(meta): binary.add((mod_name, tbl_name)) elif is_ifd_table(meta): ifd.add((mod_name, tbl_name)) + # Import lazily: serial_directory imports this module for + # shared literal helpers. At this point codegen is fully + # initialized, and using the same source descriptor avoids a + # second, name-only ProcessSerialData classifier. + try: + import serial_directory + except ImportError: + continue + try: + descriptor = serial_directory.compile_serial_inventory(mod_name, tbl_name, tbl) + except serial_directory.SerialDirectoryRefused: + continue + if not descriptor["gate_a"]["blocked_by"]: + serial[(mod_name, tbl_name)] = descriptor["processor"] arrays = (doc["modules"].get("MakerNotes") or {}).get("arrays") or {} main = arrays.get("Main") if isinstance(arrays, dict) else None rows = main.get("rows") if isinstance(main, dict) else None - return cls(ifd, binary, rows if isinstance(rows, list) and rows else None) + return cls( + ifd, + binary, + rows if isinstance(rows, list) and rows else None, + serial, + processors, + doc.get("subdirectory_validate_functions"), + doc.get("native_reader_contracts"), + ) def is_makernotes_dispatch(self, variants): """`variants` is the `\\@MakerNotes::Main` array itself (see the class doc). Absent from the dump -> never (a missing fact refuses).""" return self.makernotes_main is not None and variants == self.makernotes_main - def target_kind(self, module, table): + @staticmethod + def _same_processor(left, right): + """Whether two captured CODE operands select the same native body. + + `SubDirectory.ProcessProc` is an override, not a hint. The target + serial descriptor was compiled from the target table's processor, so + a different override cannot be sent to it. Source name, deparse and + source provenance are all required; shallow CODE facts safely fail. + """ + if not isinstance(left, dict) or not isinstance(right, dict): + return False + fields = ("__perl", "resolved", "__name", "__deparse", "source_file", "source_sha256") + return all(left.get(field) == right.get(field) for field in fields) and left.get("resolved") is True + + def target_kind(self, module, table, override=None): + target = (module, table) + if target in self.serial_tables: + native = self.table_processors.get(target) + if override is None or self._same_processor(override, native): + return "serial" + return "other" if (module, table) in self.ifd_tables: return "ifd" if (module, table) in self.binary_tables: return "binary" return "other" + def serial_target(self, module, table): + return (module, table) in self.serial_tables + + def compiled_validation(self, expression): + """Compile an IFD validation only with a live reader contract.""" + try: + compiled = directory_validation.compile_validation( + expression, self.validation_helpers, self.reader_contracts + ) + except directory_validation.ValidationRefused: + return None + # `None` means helper arguments parsed but the isolated reader did + # not authenticate the *loaded* primitive. Keeping this edge unwalked + # is required; source shape alone is not an execution permission. + return compiled if compiled.reader_contract_sha256 is not None else None + def compile_ifd_subdir(tag, stats, ctx, enclosing=None): """A tag's `SubDirectory` (tag already flag-expanded) as `Some(IfdSubdirEdge @@ -2246,7 +2316,11 @@ def compile_ifd_subdir(tag, stats, ctx, enclosing=None): return "None" pp = sd.get("ProcessProc") - if pp is not None: + target_kind = ctx.target_kind(module, table, pp) + if pp is not None and ctx.serial_target(module, table) and target_kind != "serial": + pp_name = pp.get("__name") if isinstance(pp, dict) else (pp if isinstance(pp, str) else None) + unwalked.append(f"ProcessProc override differs from target {pp_name or ''}") + elif pp is not None and target_kind != "serial": pp_name = pp.get("__name") if isinstance(pp, dict) else (pp if isinstance(pp, str) else None) if not (pp_name or "").endswith("ProcessBinaryData"): unwalked.append(f"ProcessProc {pp_name or ''}") @@ -2320,14 +2394,24 @@ def compile_ifd_subdir(tag, stats, ctx, enclosing=None): dir_src = "None" if dir_name is None else f'Some("{rust_str(dir_name)}")' validate = sd.get("Validate") is not None + validation_src = "None" if validate: - # Emitted, so the reachability census sees the edge; the walk refuses - # it (`validate: true`), because `#### eval Validate ($val, $dirData, - # $subdirStart, $size)` (Exif.pm:7082) is Perl over the directory bytes. - stats["ifd_subdir_refused_validate"] += 1 + # The existing IFD/binary paths deliberately retain their historical + # refusal. Only a source-selected serial target may opt into the + # independently authenticated U16 comparison primitive. + compiled = ctx.compiled_validation(sd.get("Validate")) if target_kind == "serial" else None + if compiled is None: + # Emitted, so the reachability census sees the edge; the walk + # refuses it (`validate: true`) because `#### eval Validate + # ($val, $dirData, $subdirStart, $size)` (Exif.pm:7082) is Perl + # over directory bytes. + stats["ifd_subdir_refused_validate"] += 1 + else: + validation_src = compiled.rust(rust_str) + stats["ifd_subdir_validate_compiled"] += 1 stats["ifd_subdir_edge_modeled"] += 1 - stats[f"ifd_subdir_edge_target_{ctx.target_kind(module, table)}"] += 1 + stats[f"ifd_subdir_edge_target_{target_kind}"] += 1 if sub_ifd: stats["ifd_subdir_edge_sub_ifd"] += 1 if perl_truthy(tag.get("MakerNotes")) or "MakerNotes" in extra: @@ -2355,7 +2439,8 @@ def compile_ifd_subdir(tag, stats, ctx, enclosing=None): f"start: {start_src}, base: {base_src}, byte_order: {byte_order_src}, " f"fix_format: {fix_src}, sub_ifd: {'true' if sub_ifd else 'false'}, " f"max_subdirs: {max_src}, dir_name: {dir_src}, " - f"validate: {'true' if validate else 'false'}, " + f"validate: {'true' if validate else 'false'}, validation: {validation_src}, " + f"processor: IfdSubdirProcessor::{'Serial' if target_kind == 'serial' else 'Native'}, " f"unwalked: {unwalked_src} }})" ) @@ -2848,9 +2933,11 @@ def gen_ifd_tables(doc, module_names, verified_exprs): #[allow(unused_imports)] use super::ifd_schema::{ IfdByteOrder, IfdFlags, IfdStart, IfdSubdirEdge, IfdTable, IfdTag, IfdVariantGroup, - RawConvEffect, + IfdSubdirProcessor, RawConvEffect, }; #[allow(unused_imports)] +use super::validation::{SizeExpectation, U16SizeCheck}; +#[allow(unused_imports)] use super::subdir::BaseExpr; #[allow(unused_imports)] use super::{ExprId, Fmt, GateA, Omitted, OtherId, PrintConv, TagGroups}; @@ -2890,10 +2977,12 @@ def gen_ifd_tables(doc, module_names, verified_exprs): ("SubDirectory edges modeled", "ifd_subdir_edge_modeled"), (" target is an IFD-style table", "ifd_subdir_edge_target_ifd"), (" target is a ProcessBinaryData table", "ifd_subdir_edge_target_binary"), + (" target is an authenticated ProcessSerialData table", "ifd_subdir_edge_target_serial"), (" target is neither (no transcribed layout)", "ifd_subdir_edge_target_other"), (" Flags SubIFD / FixFormat ifd (sub_ifd: true)", "ifd_subdir_edge_sub_ifd"), (" on a MakerNotes-marked tag (base fixing applies beyond it)", "ifd_subdir_edge_makernotes"), - (" carrying Validate (emitted; the walk refuses it)", "ifd_subdir_refused_validate"), + (" Validate compiled as an authenticated U16 size check (serial targets only)", "ifd_subdir_validate_compiled"), + (" carrying Validate without that proof (emitted; the walk refuses it)", "ifd_subdir_refused_validate"), (" no TagTable = the enclosing table (emitted unwalked; not disqualifying)", "ifd_subdir_same_table_unwalked"), (" ProcessProc other than ProcessBinaryData (emitted unwalked; not disqualifying)", "ifd_subdir_processproc_unwalked"), )), diff --git a/tools/exiftool-tables/test_codegen_ifd.py b/tools/exiftool-tables/test_codegen_ifd.py index 2867e0353..2d98763b4 100644 --- a/tools/exiftool-tables/test_codegen_ifd.py +++ b/tools/exiftool-tables/test_codegen_ifd.py @@ -418,7 +418,11 @@ def test_max_subdirs_dir_name_validate(self): sd = {"TagTable": "Image::ExifTool::Olympus::Equipment", "Start": "$val", "MaxSubdirs": "10", "DirName": "SubIFD", "Validate": "$val =~ /^\\0/"} src, stats = self._edge(sd, Flags="SubIFD") - self.assertIn('max_subdirs: Some(10), dir_name: Some("SubIFD"), validate: true, unwalked: None }', src) + self.assertIn( + 'max_subdirs: Some(10), dir_name: Some("SubIFD"), validate: true, ' + 'validation: None, processor: IfdSubdirProcessor::Native, unwalked: None }', + src, + ) # Emitted AND counted: the walk refuses it, the census still sees it. self.assertEqual(stats["ifd_subdir_edge_modeled"], 1) self.assertEqual(stats["ifd_subdir_refused_validate"], 1) @@ -439,7 +443,13 @@ def test_process_proc(self): pp = {"__perl": "CODE", "__name": name} src, stats = self._edge({"TagTable": "Image::ExifTool::Olympus::Equipment", "ProcessProc": pp}) self.assertIn('module: "Olympus", table: "Equipment", ', src) - self.assertTrue(src.endswith(f'validate: false, unwalked: Some("ProcessProc {name}") }})'), src) + self.assertTrue( + src.endswith( + f'validate: false, validation: None, processor: IfdSubdirProcessor::Native, ' + f'unwalked: Some("ProcessProc {name}") }})' + ), + src, + ) self.assertEqual( _plain(stats), {"ifd_subdir_processproc_unwalked": 1, "ifd_subdir_edge_modeled": 1, @@ -452,6 +462,83 @@ def test_process_proc(self): self.assertNotIn("ifd_subdir_processproc_unwalked", codegen.GATE_A_DISQUALIFYING) self.assertNotIn("ifd_subdir_refused_processproc", codegen.GATE_A_DISQUALIFYING) + def test_authenticated_serial_target_compiles_the_shared_u16_validation(self): + # The edge's target is selected by its captured target-table processor; + # no source module/table/tag spelling participates in this admission. + from test_directory_validation import CALL, NAME + from test_native_reader_contract import bound_helper, snapshot + + reader = snapshot() + processor = { + "__perl": "CODE", "resolved": True, + "__name": "Image::ExifTool::Any::ProcessSerialData", + "__deparse": "closed processor body", + "source_file": "Image/ExifTool/Any.pm", "source_sha256": "4" * 64, + } + ctx = codegen.IfdGenContext( + ifd_tables=set(), binary_tables=set(), + serial_tables={("Any", "Child"): processor}, + table_processors={("Any", "Child"): processor}, + validation_helpers={NAME: bound_helper(reader)}, + reader_contracts={"unsigned16": reader}, + ) + stats = codegen.new_ifd_stats() + src = codegen.compile_ifd_subdir( + {"SubDirectory": { + "TagTable": "Image::ExifTool::Any::Child", "Validate": CALL, + }}, + stats, + ctx, + ) + self.assertIn("validation: Some(U16SizeCheck { offset: 0, expected: &[SizeExpectation::Relative(0)]", src) + self.assertIn("processor: IfdSubdirProcessor::Serial", src) + self.assertEqual(_plain(stats), { + "ifd_subdir_edge_modeled": 1, + "ifd_subdir_edge_target_serial": 1, + "ifd_subdir_validate_compiled": 1, + }) + + def test_serial_override_or_helper_source_change_refuses_execution(self): + from test_directory_validation import CALL, NAME + from test_native_reader_contract import bound_helper, snapshot + + reader = snapshot() + processor = { + "__perl": "CODE", "resolved": True, + "__name": "Image::ExifTool::Any::ProcessSerialData", + "__deparse": "closed processor body", + "source_file": "Image/ExifTool/Any.pm", "source_sha256": "4" * 64, + } + ctx = codegen.IfdGenContext( + ifd_tables=set(), binary_tables=set(), + serial_tables={("Any", "Child"): processor}, + table_processors={("Any", "Child"): processor}, + validation_helpers={NAME: bound_helper(reader)}, + reader_contracts={"unsigned16": reader}, + ) + def emit(subdir): + stats = codegen.new_ifd_stats() + return codegen.compile_ifd_subdir({"SubDirectory": subdir}, stats, ctx), stats + + changed = dict(processor, source_sha256="5" * 64) + src, stats = emit({ + "TagTable": "Image::ExifTool::Any::Child", "ProcessProc": changed, + "Validate": CALL, + }) + self.assertIn("processor: IfdSubdirProcessor::Native", src) + self.assertIn("validation: None", src) + self.assertIn("ProcessProc override differs from target", src) + self.assertEqual(stats["ifd_subdir_refused_validate"], 1) + + broken_helpers = dict(ctx.validation_helpers) + broken = dict(broken_helpers[NAME]) + broken["__deparse"] = broken["__deparse"].replace("Get16u", "Get32u") + ctx.validation_helpers = {NAME: broken} + src, stats = emit({"TagTable": "Image::ExifTool::Any::Child", "Validate": CALL}) + self.assertIn("processor: IfdSubdirProcessor::Serial", src) + self.assertIn("validation: None", src) + self.assertEqual(stats["ifd_subdir_refused_validate"], 1) + def test_base_through_the_existing_grammar(self): src, _ = self._edge({"TagTable": "Image::ExifTool::Olympus::Equipment", "Start": "$valuePtr + 8", "Base": "$start - 8"}) @@ -499,7 +586,8 @@ def test_tag_with_a_refused_edge_keeps_the_flag(self): self.assertIn("subdir: Some(IfdSubdirEdge { module: \"Olympus\", table: \"Equipment\", " "start: IfdStart::ValuePtr(0), base: None, byte_order: IfdByteOrder::Inherit, " "fix_format: None, sub_ifd: false, max_subdirs: None, dir_name: None, " - "validate: false, unwalked: None }) }", src) + "validate: false, validation: None, processor: IfdSubdirProcessor::Native, " + "unwalked: None }) }", src) class VariantGroups(unittest.TestCase): @@ -648,7 +736,8 @@ def test_no_tag_table_is_the_enclosing_table_emitted_unwalked(self): src, 'Some(IfdSubdirEdge { module: "Exif", table: "Main", start: IfdStart::Val(0), base: None, ' "byte_order: IfdByteOrder::Inherit, fix_format: None, sub_ifd: true, max_subdirs: None, " - 'dir_name: Some("ExifIFD"), validate: false, ' + 'dir_name: Some("ExifIFD"), validate: false, validation: None, ' + 'processor: IfdSubdirProcessor::Native, ' 'unwalked: Some("same-table recursion (TagTable absent)") })', ) self.assertEqual( From 8bd2fb37c597b0696e502a987bb52cbed5aba5cf Mon Sep 17 00:00:00 2001 From: swackhamer Date: Sun, 13 Sep 2026 09:41:02 -0500 Subject: [PATCH 02/12] feat(tables): bridge authenticated IFD serial edges --- src/exiftool_tables/enabled_serial.rs | 42 ++ src/exiftool_tables/ifd_engine.rs | 407 +++++++++++++++++- src/exiftool_tables/mod.rs | 6 +- src/parsers/tiff/makernotes/canon.rs | 12 + .../tiff/makernotes/canon/main_engine.rs | 80 +++- tools/exiftool-tables/test_verify_ifd.py | 28 ++ tools/exiftool-tables/verify.py | 20 +- 7 files changed, 556 insertions(+), 39 deletions(-) create mode 100644 src/exiftool_tables/enabled_serial.rs diff --git a/src/exiftool_tables/enabled_serial.rs b/src/exiftool_tables/enabled_serial.rs new file mode 100644 index 000000000..c41edf461 --- /dev/null +++ b/src/exiftool_tables/enabled_serial.rs @@ -0,0 +1,42 @@ +//! Gate B for source-described `ProcessSerialData` tables. +//! +//! A serial descriptor has no carrier by itself. This allowlist is the +//! separate rollout decision that permits a supported carrier to execute one +//! table after the caller has already authenticated the parent edge. + +use super::serial_schema::SerialTable; + +/// Serial tables whose callers may execute them. Sorted and deliberately +/// narrow: the IFD-to-serial bridge currently has parent-carrier evidence only +/// for Canon::AFInfo2. Presence here is insufficient without the caller's +/// source-authenticated processor and validation facts. +pub static ENABLED_SERIAL: &[(&str, &str)] = &[("Canon", "AFInfo2")]; + +/// Gate B plus the generated table's Gate A. +#[must_use] +pub fn is_enabled(table: &SerialTable) -> bool { + table.gate_a.passes() + && ENABLED_SERIAL + .binary_search_by(|(module, name)| (*module, *name).cmp(&(table.module, table.table))) + .is_ok() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::exiftool_tables::find_serial_table; + + #[test] + fn allowlist_is_sorted_and_resolves_to_gate_a_clean_tables() { + assert!(ENABLED_SERIAL.windows(2).all(|pair| pair[0] < pair[1])); + for (module, table) in ENABLED_SERIAL { + let table = find_serial_table(module, table) + .expect("serial Gate B entry must name a generated table"); + assert!( + table.gate_a.passes(), + "{module}::{table:?} is Gate A blocked" + ); + assert!(is_enabled(table)); + } + } +} diff --git a/src/exiftool_tables/ifd_engine.rs b/src/exiftool_tables/ifd_engine.rs index 6939e7b07..584835490 100644 --- a/src/exiftool_tables/ifd_engine.rs +++ b/src/exiftool_tables/ifd_engine.rs @@ -91,7 +91,8 @@ //! a tag means; the rest are inert for the makernote tables slice I-2 //! enables. //! * `Validate` (Exif.pm:7081-7085) is Perl evaluated against the directory -//! bytes; an edge carrying it is never walked. +//! bytes. It remains unwalked unless code generation authenticated the +//! closed U16-size helper and native reader contract for a serial target. //! * `Start => '$val'` on a tag WITHOUT `Flags => 'SubIFD'`: its value is //! read as `undef` (Exif.pm:6733), so `eval('$val')` yields a byte string //! and `IsInt` fails (Exif.pm:6957-6959) unless the bytes happen to spell @@ -148,6 +149,7 @@ use crate::core::TagValue; use crate::io::ByteOrder; use super::cond::{self, MemberValue}; +use super::enabled_serial; use super::engine::{self, Dir, Emitted, Guard}; use super::exprs; use super::ifd_schema::{ @@ -155,7 +157,10 @@ use super::ifd_schema::{ }; use super::runtime::{self, DecodedValue, decode_value_of}; use super::subdir::BaseExpr; -use super::{Fmt, find_ifd_table, find_table}; +use super::{ + Fmt, SerialDir, SerialEmissionSink, SerialTable, SerialWalkResult, find_ifd_table, + find_serial_table, find_table, process_serial_directory, +}; #[path = "subdirectory_adapter.rs"] pub mod subdirectory_adapter; @@ -773,6 +778,21 @@ pub enum EntryRead { Unread, } +/// What an authenticated serial `SubDirectory` edge did for one root entry. +/// +/// `Handled` includes native no-output paths such as a false parent condition, +/// a failed source-authenticated validator, an empty child, and a selected +/// serial record that ends at a normal unmatched alternative. `Fallback` is +/// reserved for OxiDex's inability to authenticate or execute the generated +/// route (Gate A/B, missing table/proof, or tainted serial walk). A carrier +/// with a legacy reader can therefore retain its prior producer only in the +/// latter case, without converting a native omission into invented output. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SerialSubdirRead { + Handled, + Fallback, +} + /// What [`process_exif_decoded`] reports about the ROOT directory, beside /// the rows themselves. #[derive(Clone, Debug, Default, PartialEq, Eq)] @@ -783,6 +803,16 @@ pub struct RootReads { /// index in `out` and the index of the entry that produced it. Rows a /// `SubDirectory` edge produced are not listed. pub rows: Vec<(usize, usize)>, + /// `(entry, outcome)` for source-selected serial child edges from this + /// root. This stays separate from [`Self::rows`]: emitted child rows do + /// not declare a name under their parent IFD tag, while a caller still + /// needs to place or suppress its legacy parent producer in entry order. + pub serial_subdirs: Vec<(usize, SerialSubdirRead)>, + /// `(row, entry)` for every serial-child row emitted while processing a + /// root edge. These rows are deliberately not mixed into [`Self::rows`]: + /// their names belong to the child table, while the index supplies the + /// parent-entry ordering a legacy carrier needs for a safe migration. + pub serial_rows: Vec<(usize, usize)>, } /// [`process_exif`], also reporting what the walk did with each entry of @@ -848,6 +878,8 @@ fn walk( reads.entries.clear(); reads.entries.resize(entries.len(), EntryRead::Unread); reads.rows.clear(); + reads.serial_subdirs.clear(); + reads.serial_rows.clear(); } // Every entry from `from` on is one ExifTool never reaches. let refuse_rest = |decoded: &mut Option<&mut RootReads>, from: usize| { @@ -913,6 +945,13 @@ fn walk( condition_resolved, }) = resolve(table, entry, &located, ctx) else { + if direct_serial_no_match(table, entry.tag_id) { + if let Some(reads) = decoded.as_deref_mut() { + reads + .serial_subdirs + .push((index, SerialSubdirRead::Handled)); + } + } continue; }; // ExifTool.pm:9180-9186: an `Unknown` tag is not returned unless @@ -934,7 +973,16 @@ fn walk( // (Exif.pm:7103-7104 `next unless $doMaker ...`, and the `MakerNotes` // option is off), so the edge is the whole of the tag. if let Some(edge) = &tag.subdir { - descend(table, tag, edge, &located, &dir, ctx, guard, out); + let out_before = out.len(); + let outcome = descend(table, tag, edge, &located, &dir, ctx, guard, out); + if let DescendOutcome::Serial(outcome) = outcome + && let Some(reads) = decoded.as_deref_mut() + { + reads.serial_subdirs.push((index, outcome)); + reads + .serial_rows + .extend((out_before..out.len()).map(|row| (row, index))); + } continue; } // Exif.pm:6729-6745. @@ -1381,6 +1429,64 @@ fn test_hook_table(_module: &str, _table: &str) -> Option<&'static IfdTable> { None } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum DescendOutcome { + Native, + Serial(SerialSubdirRead), +} + +/// The IFD caller has no public Unknown-output option yet. It therefore +/// preserves the existing default projection: serial Unknown slots select and +/// consume bytes but do not call `FoundTag`. A caller that exposes that option +/// must carry it into this sink before widening the route. +struct IfdSerialSink<'a> { + out: &'a mut Vec, +} + +impl SerialEmissionSink for IfdSerialSink<'_> { + fn emit(&mut self, row: Emitted) { + self.out.push(row); + } + + fn serial_enabled(&self, table: &'static SerialTable) -> bool { + enabled_serial::is_enabled(table) + } +} + +/// Publish a serial child's buffered rows only when its entire native walk +/// stayed authenticated. A later unsupported state action invalidates an +/// earlier prefix as well: callers must take their documented fallback rather +/// than mixing two producers for one parent entry. +fn finish_serial_child( + out: &mut Vec, + child: Vec, + result: &SerialWalkResult, +) -> SerialSubdirRead { + if result.gate_a_blocked != 0 || result.gate_b_blocked != 0 || result.tainted { + SerialSubdirRead::Fallback + } else { + out.extend(child); + SerialSubdirRead::Handled + } +} + +/// A direct source candidate whose condition did not select an alternative. +/// It is safe to call that a handled serial omission only when the declared +/// row itself has a fully modeled condition and one source-selected serial +/// edge. Variants and unmodeled conditions remain `Unread`, preserving the +/// legacy producer rather than inferring an absence from a partial view. +fn direct_serial_no_match(table: &'static IfdTable, id: u16) -> bool { + let Some(tag) = table.tags.iter().find(|tag| tag.id == id) else { + return false; + }; + tag.condition.is_some() + && !tag.omitted.condition + && matches!( + tag.subdir.as_ref().map(|edge| edge.processor), + Some(IfdSubdirProcessor::Serial) + ) +} + /// Exif.pm:6919-7102 -- open the directory (or directories) an entry points /// at and process each with the right table. #[allow(clippy::too_many_arguments)] @@ -1393,21 +1499,32 @@ fn descend( ctx: &mut cond::Ctx, guard: &mut Guard, out: &mut Vec, -) { - // Exif.pm:7081-7085: `Validate` is Perl over the directory bytes. - if edge.validate { - return; +) -> DescendOutcome { + // Legacy IFD/binary Validate remains unwalked. A serial edge may proceed + // only when codegen carried the independently authenticated primitive; + // a schema mismatch is a carrier fallback, never an implicit approval. + if edge.validate && edge.processor == IfdSubdirProcessor::Native { + return DescendOutcome::Native; + } + if edge.validate && edge.validation.is_none() { + return DescendOutcome::Serial(SerialSubdirRead::Fallback); } // Slice IFD1: the generator emitted the edge but marked it unwalked -- // the enclosing table itself (no TagTable, Exif.pm:6939-6944) or a // ProcessProc the walk cannot run. Same outcome as `validate`: the // pointer marks its place and nothing behind it is read. if edge.unwalked.is_some() { - return; + return match edge.processor { + IfdSubdirProcessor::Native => DescendOutcome::Native, + IfdSubdirProcessor::Serial => DescendOutcome::Serial(SerialSubdirRead::Fallback), + }; } // Exif.pm:6921-6926 -- "don't process empty subdirectories". if located.bytes.is_empty() { - return; + return match edge.processor { + IfdSubdirProcessor::Native => DescendOutcome::Native, + IfdSubdirProcessor::Serial => DescendOutcome::Serial(SerialSubdirRead::Handled), + }; } let in_maker_notes = table.group0 == "MakerNotes"; let data = dir.data; @@ -1420,29 +1537,43 @@ fn descend( enum Target { Ifd(&'static IfdTable), Binary(&'static super::BinaryTable), + Serial(&'static SerialTable), } - let target = if let Some(t) = ifd_target(edge.module, edge.table) { + let target = if edge.processor == IfdSubdirProcessor::Serial { + let Some(t) = find_serial_table(edge.module, edge.table) else { + return DescendOutcome::Serial(SerialSubdirRead::Fallback); + }; + Target::Serial(t) + } else if let Some(t) = ifd_target(edge.module, edge.table) { if !walkable(t) { // Opt-in (Step 28 D1): an edge never enables its target. - return; + return DescendOutcome::Native; } Target::Ifd(t) } else if let Some(t) = find_table(edge.module, edge.table) { if !t.enabled() { - return; + return DescendOutcome::Native; } Target::Binary(t) } else { // Not a defect in the edge: the target is a table neither generator // transcribed (a custom `PROCESS_PROC`, a refused table). - return; + return DescendOutcome::Native; }; // Exif.pm:6929-6938, 7100-7101: how many times the loop runs. let iterations: Vec> = match edge.start { IfdStart::Val(_) => match pointer_values(tag, edge, located, dir.byte_order) { Some(pointers) => pointers.into_iter().map(Some).collect(), - None => return, + // Native cannot open a child when the pointer value is not + // readable. This is a handled no-child path, not a reason for a + // legacy producer to invent a second interpretation of it. + None => { + return match edge.processor { + IfdSubdirProcessor::Native => DescendOutcome::Native, + IfdSubdirProcessor::Serial => DescendOutcome::Serial(SerialSubdirRead::Handled), + }; + } }, // A `$valuePtr` start does not depend on `$val`; with `MaxSubdirs` // ExifTool would loop over the value's pieces re-opening the SAME @@ -1453,6 +1584,7 @@ fn descend( let dir_name = subdir_name(tag, edge, in_maker_notes); + let mut serial_outcome = SerialSubdirRead::Handled; for pointer in iterations { // Exif.pm:6951-6968 -- `#### eval Start ($valuePtr, $val)`, then // `$newStart -= $subdirDataPos` back to data-relative. @@ -1460,13 +1592,26 @@ fn descend( (IfdStart::ValuePtr(offset), _) => value_pos.saturating_add(offset), (IfdStart::Val(offset), Some(pointer)) => { // `$val + base` is where the pointer lands in `data`; with - // no correction the block cannot be located. + // no correction the shared reader cannot locate the block. + // This is an execution prerequisite, so retain the legacy + // producer rather than calling a failed source evaluation a + // native omission. let Some(base) = dir.base else { - return; + return match edge.processor { + IfdSubdirProcessor::Native => DescendOutcome::Native, + IfdSubdirProcessor::Serial => { + DescendOutcome::Serial(SerialSubdirRead::Fallback) + } + }; }; pointer.saturating_add(offset).saturating_add(base) } - (IfdStart::Val(_), None) => return, + (IfdStart::Val(_), None) => { + return match edge.processor { + IfdSubdirProcessor::Native => DescendOutcome::Native, + IfdSubdirProcessor::Serial => DescendOutcome::Serial(SerialSubdirRead::Handled), + }; + } }; // Exif.pm:6964-6966: `$size -= $newStart - $subdirStart` unless // SubIFD (or BadOffset, not modelled) -- the DirLen a binary target @@ -1478,10 +1623,16 @@ fn descend( }; // Exif.pm:7017-7037: "Bad SubDirectory start" ends the loop (`last`). if start < 0 || start.saturating_add(2) > data_len { - return; + return match edge.processor { + IfdSubdirProcessor::Native => DescendOutcome::Native, + IfdSubdirProcessor::Serial => DescendOutcome::Serial(SerialSubdirRead::Handled), + }; } let Ok(start_pos) = usize::try_from(start) else { - return; + return match edge.processor { + IfdSubdirProcessor::Native => DescendOutcome::Native, + IfdSubdirProcessor::Serial => DescendOutcome::Serial(SerialSubdirRead::Handled), + }; }; // Exif.pm:6971-6997. let byte_order = match edge.byte_order { @@ -1490,7 +1641,14 @@ fn descend( IfdByteOrder::Big => ByteOrder::Big, IfdByteOrder::Unknown => match detect_byte_order(data, start_pos, dir.byte_order) { Some(order) => order, - None => return, + None => { + return match edge.processor { + IfdSubdirProcessor::Native => DescendOutcome::Native, + IfdSubdirProcessor::Serial => { + DescendOutcome::Serial(SerialSubdirRead::Fallback) + } + }; + } }, }; // Exif.pm:6999-7004 -- `#### eval Base ($start,$base)` with `$start` @@ -1502,14 +1660,26 @@ fn descend( None => dir.base, Some(expr) => { if mentions_base(expr) { - return; + return match edge.processor { + IfdSubdirProcessor::Native => DescendOutcome::Native, + IfdSubdirProcessor::Serial => { + DescendOutcome::Serial(SerialSubdirRead::Fallback) + } + }; } match dir.base { Some(base) => Some(base.saturating_add(expr.eval(start - base, 0))), // `$start` is unknowable without a correction; a // constant leaves the correction unknown too. None if matches!(expr, BaseExpr::Const(_)) => None, - None => return, + None => { + return match edge.processor { + IfdSubdirProcessor::Native => DescendOutcome::Native, + IfdSubdirProcessor::Serial => { + DescendOutcome::Serial(SerialSubdirRead::Fallback) + } + }; + } } } }; @@ -1573,8 +1743,63 @@ fn descend( ); guard.depth -= 1; } + Target::Serial(target) => { + let Ok(dir_len) = usize::try_from(dir_len) else { + serial_outcome = SerialSubdirRead::Handled; + continue; + }; + if edge.validate { + let Ok(validation_size) = u32::try_from(dir_len) else { + serial_outcome = SerialSubdirRead::Handled; + continue; + }; + if !edge + .validation + .expect("checked before serial descent") + .matches(data, start_pos, validation_size, byte_order) + { + // Native Validate false is an ordinary handled omission; + // it must not reactivate a legacy child reader. + serial_outcome = SerialSubdirRead::Handled; + continue; + } + } + if !guard.admit(binary_addr(base, start_pos), table_key(target), false) { + serial_outcome = SerialSubdirRead::Handled; + continue; + } + // A serial state refusal may occur after an earlier selected + // row. Its result is a legacy fallback, so do not expose a + // prefix from the unauthenticated walk beside the hand + // producer. Commit the child rows only after the whole + // directory is known clean. + let mut serial_rows = Vec::new(); + guard.depth += 1; + let mut sink = IfdSerialSink { + out: &mut serial_rows, + }; + let result = process_serial_directory( + target, + SerialDir { + data, + dir_start: start_pos, + dir_len, + base: 0, + data_pos: base.map_or(0, |value| -value), + byte_order, + }, + ctx, + &mut sink, + ); + guard.depth -= 1; + serial_outcome = finish_serial_child(out, serial_rows, &result); + } } } + match edge.processor { + IfdSubdirProcessor::Native => DescendOutcome::Native, + IfdSubdirProcessor::Serial => DescendOutcome::Serial(serial_outcome), + } } #[cfg(test)] @@ -1585,7 +1810,9 @@ mod tests { use super::*; use crate::exiftool_tables::cond::{CmpOp, Cond, EffectSource}; use crate::exiftool_tables::ifd_schema::IfdVariantGroup; - use crate::exiftool_tables::{ExprId, GateA, IfdFlags, Omitted, PrintConv, TagGroups}; + use crate::exiftool_tables::{ + ExprId, GateA, IfdFlags, Omitted, PrintConv, SizeExpectation, TagGroups, U16SizeCheck, + }; // -- Test-only enablement/lookup registry ---------------------------------- // @@ -2996,6 +3223,140 @@ mod tests { ); } + // A source-selected serial child is keyed only by generated module/table + // facts. The parent fixture supplies a TIFF `undef[16]` entry whose first + // child word is its declared byte size, the closed U16 validator shape. + static SERIAL_EDGE_TAGS: &[IfdTag] = &[IfdTag { + subdir: Some(IfdSubdirEdge { + module: "Canon", + table: "AFInfo2", + validation: Some(U16SizeCheck { + offset: 0, + expected: &[SizeExpectation::Relative(0)], + expression: "Test::Validate($dirData,$subdirStart,$size)", + callee: "Test::Validate", + source_file: "Image/ExifTool/Test.pm", + source_sha256: "test", + reader_contract_sha256: Some("test"), + }), + processor: IfdSubdirProcessor::Serial, + validate: true, + ..edge("AFInfo2") + }), + ..plain(0x0026, "SerialChild") + }]; + static SERIAL_EDGE: IfdTable = table("SerialEdge", SERIAL_EDGE_TAGS); + + fn serial_child_parent(order: ByteOrder, declared_size: u16) -> Vec { + let floor = trailer_at(1); + let mut child = Vec::new(); + for word in [declared_size, 2, 0, 1, 2, 3, 4, 5] { + child.extend_from_slice(&bytes16(order, word)); + } + ifd( + order, + &[entry( + order, + 0x0026, + 7, + child.len() as u32, + bytes32(order, floor as u32), + )], + &child, + ) + } + + #[test] + fn authenticated_serial_edge_checks_size_then_uses_generated_child_table() { + for order in [ByteOrder::Big, ByteOrder::Little] { + let data = serial_child_parent(order, 16); + let mut members = HashMap::new(); + let mut ctx = Ctx::new(&mut members); + let mut out = Vec::new(); + let reads = process_exif_decoded( + &SERIAL_EDGE, + IfdDir { + data: &data, + ifd_start: 0, + base: Some(0), + byte_order: order, + group1: None, + }, + &mut ctx, + &mut out, + ) + .expect("the parent IFD is valid"); + assert_eq!( + reads.serial_subdirs, + vec![(0, SerialSubdirRead::Handled)], + "{order:?}: a supported child is a handled source route" + ); + assert_eq!( + reads.serial_rows, + (0..7).map(|row| (row, 0)).collect::>(), + "{order:?}: child rows retain their parent-entry position" + ); + assert_eq!( + values(&out), + vec![ + ( + "AFAreaMode", + TagValue::String("Single-point AF".to_string()) + ), + ("NumAFPoints", TagValue::Integer(0)), + ("ValidAFPoints", TagValue::Integer(1)), + ("CanonImageWidth", TagValue::Integer(2)), + ("CanonImageHeight", TagValue::Integer(3)), + ("AFImageWidth", TagValue::Integer(4)), + ("AFImageHeight", TagValue::Integer(5)), + ], + "{order:?}: child selection, cursor and source enums come from SerialTable" + ); + } + } + + #[test] + fn failed_serial_size_check_is_a_handled_native_omission() { + let data = serial_child_parent(ByteOrder::Little, 15); + let mut members = HashMap::new(); + let mut ctx = Ctx::new(&mut members); + let mut out = Vec::new(); + let reads = process_exif_decoded( + &SERIAL_EDGE, + IfdDir { + data: &data, + ifd_start: 0, + base: Some(0), + byte_order: ByteOrder::Little, + group1: None, + }, + &mut ctx, + &mut out, + ) + .expect("the parent IFD is valid"); + assert!( + out.is_empty(), + "native Validate false produces no child rows" + ); + assert_eq!(reads.serial_subdirs, vec![(0, SerialSubdirRead::Handled)]); + assert!(reads.serial_rows.is_empty()); + } + + #[test] + fn tainted_serial_child_discards_an_earlier_buffered_prefix() { + let mut out = vec!["parent"]; + let outcome = finish_serial_child( + &mut out, + vec!["serial-prefix"], + &SerialWalkResult { + tainted: true, + ..SerialWalkResult::default() + }, + ); + assert_eq!(outcome, SerialSubdirRead::Fallback); + assert_eq!(out, vec!["parent"]); + } + // FujiFilm.pm:709-714 and Olympus.pm:809-822 supply direct (not // `_variants`) member conditions. `process_exif` must use the caller's // file-level state for both string predicate forms; a missing or wrong diff --git a/src/exiftool_tables/mod.rs b/src/exiftool_tables/mod.rs index 65c271b3f..2d8220aa8 100644 --- a/src/exiftool_tables/mod.rs +++ b/src/exiftool_tables/mod.rs @@ -40,6 +40,7 @@ pub mod binary_tables; pub mod cond; pub mod enabled; pub mod enabled_ifd; +pub mod enabled_serial; pub mod engine; pub mod exprs; pub mod ifd_engine; @@ -65,10 +66,11 @@ pub use cond::{ }; pub use enabled::{ENABLED, is_enabled}; pub use enabled_ifd::ENABLED_IFD; +pub use enabled_serial::{ENABLED_SERIAL, is_enabled as is_serial_enabled}; pub use engine::{Cursor, Dir, Emitted, Step, process_binary_data, read_value}; pub use ifd_engine::{ - EntryRead, IfdDir, IfdEntry, MAX_IFD_ENTRIES, RootReads, process_exif, process_exif_decoded, - read_ifd, + EntryRead, IfdDir, IfdEntry, MAX_IFD_ENTRIES, RootReads, SerialSubdirRead, process_exif, + process_exif_decoded, read_ifd, }; pub use ifd_schema::{ IfdByteOrder, IfdFlags, IfdStart, IfdSubdirEdge, IfdSubdirProcessor, IfdTable, IfdTag, diff --git a/src/parsers/tiff/makernotes/canon.rs b/src/parsers/tiff/makernotes/canon.rs index b8fe2c2d3..259243336 100644 --- a/src/parsers/tiff/makernotes/canon.rs +++ b/src/parsers/tiff/makernotes/canon.rs @@ -5328,6 +5328,18 @@ fn parse_canon_makernote_directory( // returning whatever tags we found even if parsing isn't perfect let _ = parse_ifd_entries(data, byte_order, &config, |entry, ifd_data| { hand_entries += 1; + let entry_index = hand_entries - 1; + // A source-selected serial child keeps its parent entry index from + // the IFD walk. Only a handled native result retires this arm: an + // unavailable/tainted route falls through to the established hand + // producer, while a false parent Condition or Validate deliberately + // suppresses it with native's no-output result. + if let Some(engine) = main_engine.as_mut() + && engine.replay_serial(entry_index, &mut tags, &mut value_forms) + == Some(crate::exiftool_tables::SerialSubdirRead::Handled) + { + return; + } // A row the generated table reports and no residual arm owns: the // engine's row for this entry goes in HERE, at the entry's own // position, so a later same-named sub-table row (Processing's diff --git a/src/parsers/tiff/makernotes/canon/main_engine.rs b/src/parsers/tiff/makernotes/canon/main_engine.rs index 51ee7d951..2d2e1218f 100644 --- a/src/parsers/tiff/makernotes/canon/main_engine.rs +++ b/src/parsers/tiff/makernotes/canon/main_engine.rs @@ -68,14 +68,17 @@ //! its target lands on an allowlist -- even for another call site -- and 58 //! of them have no `Validate` to stop it, several with a live hand decoder //! (FocalLength, WBInfo, CropInfo, AspectInfo, ColorInfo, LensInfo, -//! CameraInfo*, ColorData). [`is_canon_main_row`] drops every row that is not -//! `Canon::Main`'s own, and `canon_main_edges_reach_no_enabled_table` turns -//! the day a target gets enabled into a red test instead of a silent change. +//! CameraInfo*, ColorData). [`is_canon_main_row`] drops every ordinary child +//! row. A source-authenticated serial edge is the narrow exception: the IFD +//! engine returns its rows with the parent entry index, and this buffer replays +//! them there while preserving a hand fallback for unavailable or tainted +//! execution. Other newly enabled targets still turn the fence test red. use std::collections::HashMap; use crate::exiftool_tables::{ - Ctx, Emitted, IfdDir, IfdTable, MemberValue, declares, engine_reports, process_exif, read_ifd, + Ctx, Emitted, IfdDir, IfdTable, MemberValue, SerialSubdirRead, declares, engine_reports, + process_exif_decoded, }; use crate::parsers::tiff::ifd_parser::ByteOrder; use crate::parsers::tiff::makernotes::shared::engine_value::engine_value_text; @@ -137,6 +140,9 @@ struct Row { no_print_conv: Option, low_priority: bool, consumed: bool, + /// The root entry that produced a serial child row. Root-table rows keep + /// `None` and retain their existing name-to-id replay contract. + parent_entry: Option, } /// What one engine walk of `Canon::Main` reported, held until the hand walk @@ -146,6 +152,11 @@ pub(super) struct MainEngineRows { table: &'static IfdTable, /// In emission order, which is IFD entry order. rows: Vec, + /// Source-selected serial edge result, keyed by root IFD entry index. + /// This is not inferred from child tag names: false conditions and failed + /// validators are meaningful handled omissions even when no child row + /// exists. + serial_outcomes: Vec<(usize, SerialSubdirRead)>, /// How many entries the engine's `read_ifd` accepted, `None` when it /// refused the directory (then no row exists). entries: Option, @@ -156,6 +167,7 @@ impl MainEngineRows { Self { table, rows: Vec::new(), + serial_outcomes: Vec::new(), entries: None, } } @@ -195,6 +207,33 @@ impl MainEngineRows { } } + /// Replay every child row generated for this parent entry. `Handled` + /// consumes the parent hand arm even if native produced no rows; `Fallback` + /// leaves that arm available because the shared route was not authenticated + /// or became tainted. + pub(super) fn replay_serial( + &mut self, + entry: usize, + tags: &mut HashMap, + forms: &mut Option<&mut HashMap>, + ) -> Option { + let outcome = self + .serial_outcomes + .iter() + .find_map(|(candidate, outcome)| (*candidate == entry).then_some(*outcome))?; + if outcome == SerialSubdirRead::Handled { + for row in self + .rows + .iter_mut() + .filter(|row| !row.consumed && row.parent_entry == Some(entry)) + { + row.consumed = true; + insert_row(row, tags, forms); + } + } + Some(outcome) + } + /// Rows whose entry the hand walk never reached -- `parse_ifd_entries` /// stops at the first truncated entry and refuses more than 200, while /// `read_ifd` accepts up to 512 -- in emission order. Later in IFD order @@ -286,14 +325,13 @@ pub(super) fn walk( let Some(ifd_data) = data.get(start..) else { return rows; }; - rows.entries = read_ifd(ifd_data, 0, order.to_io_byte_order()).map(|entries| entries.len()); let mut members: HashMap<&'static str, MemberValue> = HashMap::new(); if !model.is_empty() { members.insert("Model", MemberValue::Str(model.to_string())); } let mut ctx = Ctx::new(&mut members); let mut emitted = Vec::new(); - process_exif( + let Some(reads) = process_exif_decoded( table, IfdDir { data: ifd_data, @@ -304,10 +342,17 @@ pub(super) fn walk( }, &mut ctx, &mut emitted, - ); - for row in emitted { + ) else { + return rows; + }; + rows.entries = Some(reads.entries.len()); + rows.serial_outcomes = reads.serial_subdirs.clone(); + for (out_index, _) in reads.rows { + let Some(row) = emitted.get(out_index) else { + continue; + }; // FENCE: `Canon::Main`'s own rows only. See the module doc. - if !is_canon_main_row(&row) { + if !is_canon_main_row(row) { continue; } let Some(text) = engine_value_text(&row.value) else { @@ -319,6 +364,23 @@ pub(super) fn walk( no_print_conv: row.value_conv.as_ref().and_then(engine_value_text), low_priority: row.low_priority, consumed: false, + parent_entry: None, + }); + } + for (out_index, entry_index) in reads.serial_rows { + let Some(row) = emitted.get(out_index) else { + continue; + }; + let Some(text) = engine_value_text(&row.value) else { + continue; + }; + rows.rows.push(Row { + name: row.name, + text, + no_print_conv: row.value_conv.as_ref().and_then(engine_value_text), + low_priority: row.low_priority, + consumed: false, + parent_entry: Some(entry_index), }); } rows diff --git a/tools/exiftool-tables/test_verify_ifd.py b/tools/exiftool-tables/test_verify_ifd.py index bc9072b77..ce11051e2 100644 --- a/tools/exiftool-tables/test_verify_ifd.py +++ b/tools/exiftool-tables/test_verify_ifd.py @@ -262,6 +262,34 @@ def test_edge_without_the_unwalked_field_is_out_of_date(self): ' unwalked: None,', 'dir_name: Some("KodakIFD"),\n validate: false,') + def test_serial_edge_schema_requires_and_parses_processor_and_validation(self): + src = SAMPLE.read_text(encoding="utf-8") + src = src.replace("RawConvEffect,", "RawConvEffect, IfdSubdirProcessor, U16SizeCheck, SizeExpectation,", 1) + old = 'dir_name: Some("KodakIFD"),\n validate: false,\n unwalked: None,' + new = ( + 'dir_name: Some("KodakIFD"),\n validate: true,\n' + ' validation: Some(U16SizeCheck { offset: 0, expected: &[SizeExpectation::Relative(0)], ' + 'expression: "Validate($dirData,$subdirStart,$size)", callee: "Validate", ' + 'source_file: "Image/ExifTool/Canon.pm", source_sha256: "' + "a" * 64 + '", ' + 'reader_contract_sha256: Some("' + "b" * 64 + '") }),\n' + ' processor: IfdSubdirProcessor::Serial,\n unwalked: None,' + ) + self.assertEqual(src.count(old), 1) + serial_src = src.replace(old, new) + serial_src = re.sub( + r'(validate: false,\n)(\s*)unwalked:', + r'\1\2validation: None,\n\2processor: IfdSubdirProcessor::Native,\n\2unwalked:', + serial_src, + ) + parsed = _parse_text(serial_src) + edge = parsed.tags[("Exif", "Main", "33424")]["subdir"] + self.assertEqual(edge["processor"], "Serial") + self.assertTrue(edge["validate"]) + self.assertIn("U16SizeCheck", edge["validation"]) + + with self.assertRaisesRegex(SystemExit, "serial subdir schema is missing"): + _parse_text(src) + def test_rustfmt_wrapped_unwalked_reason_parses(self): # The committed regen wraps a long reason over three lines (ProfileIFD, # 0xc6f5, at the IFD1 landing-1 regen): whitespace after `Some(` and a diff --git a/tools/exiftool-tables/verify.py b/tools/exiftool-tables/verify.py index 2cba7ed41..868d4a228 100644 --- a/tools/exiftool-tables/verify.py +++ b/tools/exiftool-tables/verify.py @@ -2155,6 +2155,8 @@ def expected_subdir_edge(fact): r'max_subdirs:\s*(?PNone|Some\(\d+\))\s*,\s*' r'dir_name:\s*(?PNone|Some\("(?:[^"\\]|\\.)*"\))\s*,\s*' r'validate:\s*(?Ptrue|false)\s*,\s*' + r'(?:validation:\s*(?PNone|Some\(\s*U16SizeCheck\s*\{.*?\}\s*\))\s*,\s*' + r'processor:\s*IfdSubdirProcessor::(?PNative|Serial)\s*,\s*)?' # rustfmt wraps a long reason as `Some(\n "...",\n)`: whitespace after `Some(` and a # trailing comma before `)` are part of the committed shape (ProfileIFD 0xc6f5, whose # reason names three refusals, is the first edge long enough to wrap). @@ -2216,7 +2218,7 @@ def _parse_omitted(text, k): return out -def _parse_ifd_subdir_value(text, k): +def _parse_ifd_subdir_value(text, k, require_serial_schema=False): """`None` -> None; `Some(IfdSubdirEdge { ... })` -> its facts. The `base:` value (`None` or `Some(&BaseExpr::...)`, nested arbitrarily) is located by `_value_span` and kept as whitespace-free text.""" @@ -2231,6 +2233,8 @@ def _parse_ifd_subdir_value(text, k): tm = _IFD_SUBDIR_TAIL_RE.match(text, base_end) if not tm: raise SystemExit(f"{k}: unrecognised subdir value (tail) {text!r} {_OUT_OF_DATE}") + if require_serial_schema and tm.group("processor") is None: + raise SystemExit(f"{k}: serial subdir schema is missing validation/processor fields {_OUT_OF_DATE}") return { "module": m.group("module"), "table": m.group("table"), @@ -2242,6 +2246,9 @@ def _parse_ifd_subdir_value(text, k): "max_subdirs": _some_int(tm.group("max_subdirs")), "dir_name": _some_str_opt(tm.group("dir_name")), "validate": tm.group("validate") == "true", + "validation": (None if tm.group("validation") is None + else re.sub(r"\s+", "", tm.group("validation"))), + "processor": tm.group("processor") or "Native", "unwalked": _some_str_opt(tm.group("unwalked")), } @@ -2267,7 +2274,7 @@ class ParsedIfd(NamedTuple): structure: list -def _parse_one_ifd_tag(src, f, k, out, v2_schema): +def _parse_one_ifd_tag(src, f, k, out, v2_schema, require_serial_schema=False): if v2_schema: condition_start = f.end() condition_end = _value_span(src, condition_start) @@ -2321,7 +2328,9 @@ def _parse_one_ifd_tag(src, f, k, out, v2_schema): f"{src[pc_start:pc_end].strip()!r} {_OUT_OF_DATE}" ) subdir_end = _value_span(src, sm.end()) - tag["subdir"] = _parse_ifd_subdir_value(src[sm.end():subdir_end], k) + tag["subdir"] = _parse_ifd_subdir_value( + src[sm.end():subdir_end], k, require_serial_schema=require_serial_schema + ) _parse_print_conv( src, pc_start, pc_end, k, out.enums, out.bitmasks, out.other_ids, out.print_hexes, out.pc_kinds, ) @@ -2345,6 +2354,7 @@ def parse_ifd_rust(path): src = fh.read() out = ParsedIfd({}, {}, set(), defaultdict(dict), {}, {}, {}, {}, [], []) v2_schema = IFD_TAG_V2_RE.search(src) is not None + require_serial_schema = "IfdSubdirProcessor" in src tag_re = IFD_TAG_V2_RE if v2_schema else IFD_TAG_RE heads = list(IFD_TABLE_RE.finditer(src)) @@ -2381,7 +2391,7 @@ def parse_ifd_rust(path): k = (mod, tbl, str(int(f.group("id"), 0))) if k in out.tags: out.structure.append(f"{k}: duplicate id in `tags`") - _parse_one_ifd_tag(src, f, k, out, v2_schema) + _parse_one_ifd_tag(src, f, k, out, v2_schema, require_serial_schema) ids.append(int(f.group("id"), 0)) vm = IFD_VARIANTS_MARKER_RE.search(src, t_end, end) @@ -2398,7 +2408,7 @@ def parse_ifd_rust(path): for pos, f in enumerate(tag_re.finditer(src, a_s, a_e)): k = (mod, tbl, f"{gid}#{pos}") out.variant_keys.add(k) - _parse_one_ifd_tag(src, f, k, out, v2_schema) + _parse_one_ifd_tag(src, f, k, out, v2_schema, require_serial_schema) if int(f.group("id"), 0) != gid: out.structure.append( f"{k}: alternative carries id {f.group('id')} inside group id {gid}" From eb20b81836a4f4b2b094291320d68102087b12d1 Mon Sep 17 00:00:00 2001 From: swackhamer Date: Sun, 13 Sep 2026 09:48:43 -0500 Subject: [PATCH 03/12] test(tables): verify serial IFD validation facts --- src/exiftool_tables/ifd_engine.rs | 10 +++++ tools/exiftool-tables/oracle.pl | 6 +++ tools/exiftool-tables/test_verify_ifd.py | 44 +++++++++++++++++++++- tools/exiftool-tables/verify.py | 47 ++++++++++++++++++++++-- 4 files changed, 101 insertions(+), 6 deletions(-) diff --git a/src/exiftool_tables/ifd_engine.rs b/src/exiftool_tables/ifd_engine.rs index 584835490..5faf52f96 100644 --- a/src/exiftool_tables/ifd_engine.rs +++ b/src/exiftool_tables/ifd_engine.rs @@ -1774,6 +1774,13 @@ fn descend( // producer. Commit the child rows only after the whole // directory is known clean. let mut serial_rows = Vec::new(); + // `process_serial_directory` may set members before reaching + // a later refusal. Its caller must not then enter the legacy + // producer with a prefix of state that native never proved + // this shared route could execute. Parent selection effects + // (for example AFInfo3's condition assignment) happened + // before this snapshot and are intentionally retained. + let members_before = ctx.members.clone(); guard.depth += 1; let mut sink = IfdSerialSink { out: &mut serial_rows, @@ -1793,6 +1800,9 @@ fn descend( ); guard.depth -= 1; serial_outcome = finish_serial_child(out, serial_rows, &result); + if serial_outcome == SerialSubdirRead::Fallback { + *ctx.members = members_before; + } } } } diff --git a/tools/exiftool-tables/oracle.pl b/tools/exiftool-tables/oracle.pl index 927b176e7..d404f82b1 100755 --- a/tools/exiftool-tables/oracle.pl +++ b/tools/exiftool-tables/oracle.pl @@ -105,6 +105,8 @@ # Condition (6) # IFD MODULE TABLE KEY SUBDIR TAGTABLE START BASE PROCESSPROC BYTEORDER VALIDATE # FIXFORMAT SUBIFD MAXSUBDIRS DIRNAME (15) +# IFD MODULE TABLE KEY VALIDATION EXPRESSION CALLEE SOURCE_FILE SOURCE_SHA256 (9) +# -- scalar SubDirectory Validate provenance # # KEY is the integer tag id as ExifTool keys it, or `"$k#$i"` for the i-th # alternative of a `_variants` arrayref (same convention as the binary rows). @@ -745,6 +747,10 @@ sub emit_ifd_entry { || (defined $fix && !ref $fix && $fix eq 'ifd')) ? 1 : 0; print join("\t", @p, 'SUBDIR', $tagtable, $start, $base, $proc, $bo, $validate, dash_text($fix), $subifd, $max, $dir), "\n"; + if (ref $sd eq 'HASH' && defined $sd->{Validate} && !ref $sd->{Validate}) { + print join("\t", @p, 'VALIDATION', clean($sd->{Validate}), + keyed_validation_source($sd->{Validate})), "\n"; + } } my $pc = $e->{PrintConv}; diff --git a/tools/exiftool-tables/test_verify_ifd.py b/tools/exiftool-tables/test_verify_ifd.py index ce11051e2..6029ca14e 100644 --- a/tools/exiftool-tables/test_verify_ifd.py +++ b/tools/exiftool-tables/test_verify_ifd.py @@ -269,7 +269,8 @@ def test_serial_edge_schema_requires_and_parses_processor_and_validation(self): new = ( 'dir_name: Some("KodakIFD"),\n validate: true,\n' ' validation: Some(U16SizeCheck { offset: 0, expected: &[SizeExpectation::Relative(0)], ' - 'expression: "Validate($dirData,$subdirStart,$size)", callee: "Validate", ' + 'expression: "Image::ExifTool::Canon::Validate($dirData,$subdirStart,$size)", ' + 'callee: "Image::ExifTool::Canon::Validate", ' 'source_file: "Image/ExifTool/Canon.pm", source_sha256: "' + "a" * 64 + '", ' 'reader_contract_sha256: Some("' + "b" * 64 + '") }),\n' ' processor: IfdSubdirProcessor::Serial,\n unwalked: None,' @@ -285,11 +286,50 @@ def test_serial_edge_schema_requires_and_parses_processor_and_validation(self): edge = parsed.tags[("Exif", "Main", "33424")]["subdir"] self.assertEqual(edge["processor"], "Serial") self.assertTrue(edge["validate"]) - self.assertIn("U16SizeCheck", edge["validation"]) + self.assertEqual(edge["validation"][:4], ( + 0, (("Relative", 0),), + "Image::ExifTool::Canon::Validate($dirData,$subdirStart,$size)", + "Image::ExifTool::Canon::Validate", + )) with self.assertRaisesRegex(SystemExit, "serial subdir schema is missing"): _parse_text(src) + def test_serial_validation_requires_native_operands_and_reader_contract(self): + src = SAMPLE.read_text(encoding="utf-8") + src = src.replace("RawConvEffect,", "RawConvEffect, IfdSubdirProcessor, U16SizeCheck, SizeExpectation,", 1) + old = 'dir_name: Some("KodakIFD"),\n validate: false,\n unwalked: None,' + new = ( + 'dir_name: Some("KodakIFD"),\n validate: true,\n' + ' validation: Some(U16SizeCheck { offset: 0, expected: &[SizeExpectation::Relative(0)], ' + 'expression: "Image::ExifTool::Canon::Validate($dirData,$subdirStart,$size)", ' + 'callee: "Image::ExifTool::Canon::Validate", source_file: "Image/ExifTool/Canon.pm", ' + 'source_sha256: "' + "a" * 64 + '", reader_contract_sha256: None }),\n' + ' processor: IfdSubdirProcessor::Serial,\n unwalked: None,' + ) + serial_src = src.replace(old, new) + serial_src = re.sub( + r'(validate: false,\n)(\s*)unwalked:', + r'\1\2validation: None,\n\2processor: IfdSubdirProcessor::Native,\n\2unwalked:', + serial_src, + ) + oracle = ORACLE.replace( + "IFD\tExif\tMain\t33424\tSUBDIR\tImage::ExifTool::Kodak::IFD\t$val\t-\t-\t-\t0\t-\t1\t1\tKodakIFD", + "IFD\tExif\tMain\t33424\tSUBDIR\tImage::ExifTool::Kodak::IFD\t$val\t-\t-\t-\t1\t-\t1\t1\tKodakIFD", + ) + ( + "IFD\tExif\tMain\t33424\tVALIDATION\t" + "Image::ExifTool::Canon::Validate($dirData,$subdirStart,$size)\t" + "Image::ExifTool::Canon::Validate\tImage/ExifTool/Canon.pm\t" + "a" * 64 + "\n" + ) + failed, report = _run(_parse_text(serial_src), oracle) + self.assertGreater(failed, 0, report) + self.assertIn("validation_reader_contract", report) + + changed = _parse_text(serial_src.replace("Relative(0)", "Relative(1)", 1)) + failed, report = _run(changed, oracle) + self.assertGreater(failed, 0, report) + self.assertIn("validation:compiled validation operands or helper source differ from native", report) + def test_rustfmt_wrapped_unwalked_reason_parses(self): # The committed regen wraps a long reason over three lines (ProfileIFD, # 0xc6f5, at the IFD1 landing-1 regen): whitespace after `Some(` and a diff --git a/tools/exiftool-tables/verify.py b/tools/exiftool-tables/verify.py index 868d4a228..0f09285c5 100644 --- a/tools/exiftool-tables/verify.py +++ b/tools/exiftool-tables/verify.py @@ -2246,8 +2246,9 @@ def _parse_ifd_subdir_value(text, k, require_serial_schema=False): "max_subdirs": _some_int(tm.group("max_subdirs")), "dir_name": _some_str_opt(tm.group("dir_name")), "validate": tm.group("validate") == "true", - "validation": (None if tm.group("validation") is None - else re.sub(r"\s+", "", tm.group("validation"))), + "validation": verify_directory_validation.parse_rust( + tm.group("validation") or "None", unescape + ), "processor": tm.group("processor") or "Native", "unwalked": _some_str_opt(tm.group("unwalked")), } @@ -2491,6 +2492,8 @@ class IfdOracle(NamedTuple): hooks: set conditions: set subdirs: dict + validations: dict + reader_contracts: dict # `PCEXPR` rows: keys whose ExifTool PrintConv is a scalar expression. pcexprs: set @@ -2501,9 +2504,17 @@ def parse_ifd_oracle(out): a SystemExit: the oracle and the verifier move in lockstep, and a row silently ignored is a fact silently unverified.""" o = IfdOracle({}, {}, {}, {}, {}, defaultdict(dict), defaultdict(dict), set(), {}, {}, - {}, {}, {}, set(), set(), {}, set()) + {}, {}, {}, set(), set(), {}, {}, {}, set()) for line in out.splitlines(): p = line.split("\t") + if len(p) == 3 and p[0] == "NATIVE_READER_CONTRACT": + if p[1] in o.reader_contracts: + raise SystemExit(f"duplicate native reader contract {p[1]!r}") + try: + o.reader_contracts[p[1]] = json.loads(p[2]) + except json.JSONDecodeError as exc: + raise SystemExit(f"invalid native reader contract {p[1]!r}: {exc}") from exc + continue if p[0] != "IFD": continue n, kind = len(p), p[4] if len(p) > 4 else "" @@ -2549,6 +2560,10 @@ def parse_ifd_oracle(out): "byteorder": p[9], "validate": p[10] == "1", "fixformat": p[11], "subifd": p[12] == "1", "maxsubdirs": p[13], "dirname": p[14], } + elif kind == "VALIDATION" and n == 9: + if k in o.validations: + raise SystemExit(f"duplicate IFD validation facts for {k}") + o.validations[k] = tuple(p[5:9]) else: raise SystemExit( f"unrecognised IFD oracle row {line!r} -- oracle.pl and verify.py " @@ -2955,8 +2970,32 @@ def verify_ifd(gen, orc, show=10): diffs.append("base") if (edge["unwalked"] is not None) != want["unwalked"]: diffs.append("unwalked") + compiled_validation = edge["validation"] + native_validation = orc.validations.get(k) + if compiled_validation is not None: + if not edge["validate"]: + diffs.append("validation_without_validate") + problem = verify_directory_validation.mismatch( + compiled_validation, native_validation + ) + if problem: + diffs.append(f"validation:{problem}") + reader_sha = compiled_validation[6] + if reader_sha is None: + diffs.append("validation_reader_contract") + else: + problem = verify_native_reader.mismatch( + reader_sha, orc.reader_contracts.get("unsigned16") + ) + if problem: + diffs.append(f"validation_reader:{problem}") + elif edge["processor"] == "Serial" and edge["validate"]: + # A serial descent cannot reinterpret an opaque Perl + # Validate. A missing compiled primitive must keep the + # edge unwalked, never become an executable route. + diffs.append("serial_validate_without_authenticated_primitive") if diffs: - t_edge.miss((k, {f: edge[f] for f in diffs}, + t_edge.miss((k, {f: edge.get(f) for f in diffs}, {f: want.get(f, want.get("base_present")) for f in diffs})) else: t_edge.hit() From 757b861ee717d4be3e62fb27a659251110256ed0 Mon Sep 17 00:00:00 2001 From: swackhamer Date: Sun, 13 Sep 2026 09:53:12 -0500 Subject: [PATCH 04/12] Reproduce serial parent bridge checkpoint and record migration gates --- docs/AUTOGENERATION-PLAN.md | 2 +- docs/AUTOGENERATION-PROGRESS.md | 29 +- docs/reference/afinfo2-production-plan.md | 76 ++ docs/reference/serial-afinfo-plan.md | 9 +- src/exiftool_tables/ifd_tables.rs | 1162 ++++++++++++++++++++- 5 files changed, 1262 insertions(+), 16 deletions(-) create mode 100644 docs/reference/afinfo2-production-plan.md diff --git a/docs/AUTOGENERATION-PLAN.md b/docs/AUTOGENERATION-PLAN.md index 4b976800f..d570a41db 100644 --- a/docs/AUTOGENERATION-PLAN.md +++ b/docs/AUTOGENERATION-PLAN.md @@ -37,7 +37,7 @@ areas remain unfinished; they cannot disappear from the denominator. | Shared binary strings | Merged in PR #747 at `8f0fdaf4`. It preserves raw bytes in saved state, distinguishes a one-byte default from a remainder string, and carries the bounded CameraInfo repair. | This is a shared capability, not a Canon migration. CameraInfo remains a legacy text-domain adapter, and no Canon manual reader has been removed. The prior full-pair supervisor-status limitation remains recorded. | | Keyed-directory schema and compiler | Merged in PR #748 at `ebbe1ece`; all final hosted checks passed at `a422e8de`. | Native parent facts and expression declarations are checked. Shared reporting policy merged in #750; the inactive reader merged in #752 at `634e5616`. No production route is active. | | Shared word-directory processor | Merged in PR #754 at `1138a880`; nine tables, 132 rows, 698 Python tests and all five hosted jobs pass. | Four of five unsupported child processors now have generated descriptors. Canon production routing and manual-reader retirement remain unfinished. | -| Shared serial processor | Merged in #757 at `58849bc7`, after #755/#756. Eight tables account for 106 emitted alternatives and 26 omissions; all five hosted checks pass. | The shared reader now has a validated production caller in Real AudioV4. Canon table and parent behavior still need completion. | +| Shared serial processor | Foundation merged in #757; #759 at `8887e5d9` expands the eight-table total to 122 emitted alternatives and 10 omissions, with all five hosted checks passing. Canon AFInfo 14/14 and AFInfo2 16/16 are generated and natively replayed. | Real AudioV4 is a validated production caller. Canon's next delivery is complete parent routing and manual AFInfo2 reader retirement; definition readiness alone does not count as that migration. | | Real AudioV4 retirement | Merged in #758 at `19cb7650`; one manual reader and its 31-slot sequence removed. Full corpus: one Copyright correction, 4,237 other files unchanged. All five hosted checks pass. | Supported source-name changes reach actual output after regeneration with no tag-specific Python/Rust edit. Native occurrence groups, warning output and V3/V5 remain explicit residuals. | | Recorded source inventory | Merged in PR #749 at `18a8ef17`; all final hosted checks passed at `72e8e664`. The report accounts for 1,512 table identities and retains 119 tables with no named rows. | This establishes the captured source population. Classifying which rules are generated, manual, unsupported or unclassified remains open; source shape is not automation. | | Sony plain generator recovery | PR #745 merged; six tables and 193 rows reproduced | These tables can be rebuilt. This alone does not prove that their behavior is fully automatic. | diff --git a/docs/AUTOGENERATION-PROGRESS.md b/docs/AUTOGENERATION-PROGRESS.md index ffbb6226a..293e2c280 100644 --- a/docs/AUTOGENERATION-PROGRESS.md +++ b/docs/AUTOGENERATION-PROGRESS.md @@ -7,12 +7,18 @@ This is the working scoreboard for [the plan](AUTOGENERATION-PLAN.md). Latest combined Canon definitions: **AFInfo 14/14 and AFInfo2 16/16**, both independently verified after formatting. Across all eight serial tables, emitted alternatives rise **106 -> 122** and omissions fall **26 -> 10**. -This checkpoint is implemented, definition-validated and replayed through the -actual generated tables: all three native/Rust tests pass, as does full -regeneration. Remaining local and hosted merge checks are pending; it is not -merged or active in a Canon production carrier. +This checkpoint merged in PR #759 as `8887e5d9` at 14:22 UTC. All five +required hosted checks passed at `49a1b32d`, including 766 canonical Python +tests in 646.409 seconds, full Cargo tests and the three explicitly selected +native/Rust replays. Official regeneration and local shared-reader checks +also pass. The definitions are merged; a Canon production carrier is not yet +enabled by that merge. The [Canon plan](reference/serial-afinfo-plan.md) records the failed pipeline -attempt, its formatting repair and the remaining acceptance work. +attempt, its formatting repair and the remaining acceptance work. The active +[production migration](reference/afinfo2-production-plan.md) connects the +authenticated parent edges, verifies complete output and removes the AFInfo2 +manual reader in one accepted batch. A parent connection alone retires zero +manual output code and does not meet that delivery's goal. The following entries retain the completed milestones and their exact evidence. @@ -141,13 +147,12 @@ native warning output and AudioV3/V5 activation remain unfinished. The retired correct direct Real-RA4 values through the generated reader. No project-wide autogenerated percentage follows from that bounded count. -Next is the [Canon autofocus definition plan](reference/serial-afinfo-plan.md). -AFInfo and AFInfo2 currently account for 30 native alternatives: 15 emitted and -15 explicitly omitted, with both tables blocked. The next target is complete -source-derived representation and native execution proof for both tables. -Generic parent routing and validators follow separately; Canon's one child -processor block, four parent omissions and zero retired manual readers remain -unchanged until their respective checks pass. +The [Canon autofocus definition plan](reference/serial-afinfo-plan.md) is now +implemented and merged in #759: all 30 alternatives are generated and both +tables pass native execution checks. Generic parent routing, validator +execution and actual Canon manual-reader retirement remain open. The four +CanonRaw parent omissions are separate work; completing AFInfo2 does not +silently remove them from the inventory. ### Latest recorded percentage has an unresolved validation defect diff --git a/docs/reference/afinfo2-production-plan.md b/docs/reference/afinfo2-production-plan.md new file mode 100644 index 000000000..34b4b94f3 --- /dev/null +++ b/docs/reference/afinfo2-production-plan.md @@ -0,0 +1,76 @@ +# Replace Canon's manual AFInfo2 reader + +Updated September 13, 2026. Base: `8887e5d9` (merged PR #759). + +## Goal and finish conditions + +Both native Canon Main parent edges, `0x0026` and `0x003c`, must execute the +source-generated AFInfo2 table through the shared IFD and serial readers. +Remove the duplicate manual field sequence and its private offsets/conversion +declarations after parity is demonstrated. A bridge with the manual producer +still present is an intermediate checkpoint, not a completed migration. + +| Measure | Starting point | Required result | +| --- | --- | --- | +| Generated AFInfo2 alternatives | 16/16, independently verified | Preserve complete source accounting and fail stale artifact mutations | +| Parent edge behavior | Manual production reader; generated validation marker cannot execute | Derive effective processor, validator operands, parent condition/state, bounded child bytes and order from authenticated native facts | +| Production output | Manual field sequence | Generated child rows supply the actual caller's display and numeric values in correct parent order | +| AFInfo2 manual reader | One shared arm for two parent IDs | Remove the arm and AFInfo2-only definitions; retain helpers genuinely used by old AFInfo or CIFF | +| Parity | Exact control build at the merged base | Native/control/candidate evidence for real files and constructed boundaries; full paired corpus has no unexplained regression | +| Upgrade behavior | Table definitions can regenerate | A supported copied-source change reaches actual carrier output with no tag-specific Rust/Python edit; stale validation or processor facts are rejected | + +## Implementation + +Generate the effective child processor from its target table and any explicit +edge override. Reuse the existing authenticated unsigned-16 size validator. +The shared bridge passes the original bounded child span, byte order and +source-selected parent effects, then buffers child output until execution is +known valid. Native condition/validation omissions and unsupported execution +must remain distinguishable in the reader's result. + +An explicit activation policy transfers ownership to the generated route. +Once transferred, an execution refusal cannot silently revive a handwritten +AFInfo2 decoder. Discard speculative child rows and state on refusal, while +preserving the native parent effects that happened before child execution. +Unmigrated families remain explicit residual work. + +Native AFInfo's geometry validator is different and remains a separate +migration. The four CanonRaw parent omissions also remain counted. Do not +claim that this batch retires old AFInfo, CIFF or unrelated Canon readers. + +## Acceptance and evidence + +Use pinned ExifTool 13.59 and canonical Perl 5.38.2. Real parent discovery +uses native verbose directory traces or the actual parent IDs, because emitted +tag names do not reliably name their source table. An earlier 120-file search +for `CanonAFInfo2`/`AFInfo3` output keys was the wrong instrument; its absence +result is withdrawn. Native traces confirm AFInfo2 in Canon1DmkIII.jpg, +CanonEOS-1D_MarkIII.jpg and CanonPowerShotSX740HS.jpg. + +Constructed TIFF/MakerNote cases cover both byte orders, nonzero child starts, +both parent IDs and their ordering, EOS conditions, signed arrays/multiword +bits, zero and short counts, rejected size, four leading NUL bytes, and a +later valid sibling after refusal. Preserve warnings and failed fixture +construction attempts separately; raw callback proof is not full carrier +output proof. Existing occurrence/group limitations must be recorded against +the control and must not be relabeled as new successful output. + +Evidence is stored relative to `$OXIDEX_WORK_EVIDENCE`: + +- `shared-pilot/afinfo2-production-integration-20260913/`: exact control build, + candidate regeneration/build, comparisons, reviews and publication results. +- `shared-pilot/serial-afinfo-integration-20260913/parent-bridge-contract/`: + independent retirement checklist, native parent contract and carrier fixtures. + +Three Terra workers author the source, independently review it, and prepare +native fixtures. The coordinator alone performs combined builds and corpus +comparisons using one shared Cargo cache. Published source checkpoints are +allowed before full acceptance, with pending checks explicit; merge requires +the complete migration and its gates. + +## Current state + +Source checkpoints add the generic schema/compiler and parent bridge, plus an +independent live validation oracle. Integration and retirement are in progress. +No Canon manual reader is counted as retired yet. No project-wide generated +percentage is inferred from these two edges or this table's 16 alternatives. diff --git a/docs/reference/serial-afinfo-plan.md b/docs/reference/serial-afinfo-plan.md index 42dc66959..59c825f16 100644 --- a/docs/reference/serial-afinfo-plan.md +++ b/docs/reference/serial-afinfo-plan.md @@ -1,4 +1,11 @@ -# Next: complete the two Canon autofocus table definitions +# Canon autofocus definitions and remaining production migration + +Definition delivery merged in PR #759 as `8887e5d9` at 14:22 UTC on +September 13. All five required hosted checks pass, including 766 canonical +Python tests and all three native/Rust table replays. Both Canon tables are +complete at the definition gate. The historical checkpoints below retain +their original validation state; the current next delivery is the +[AFInfo2 production migration](afinfo2-production-plan.md). Real AudioV4 retirement merged in #758. The next goal is to make Canon AFInfo and AFInfo2 run through the same shared serial machinery, then remove their diff --git a/src/exiftool_tables/ifd_tables.rs b/src/exiftool_tables/ifd_tables.rs index 317ec052b..152179eb3 100644 --- a/src/exiftool_tables/ifd_tables.rs +++ b/src/exiftool_tables/ifd_tables.rs @@ -45,12 +45,14 @@ pub const IFD_EXIFTOOL_VERSION: &str = "13.59"; use super::cond::{CmpOp, Cond, EffectSource}; #[allow(unused_imports)] use super::ifd_schema::{ - IfdByteOrder, IfdFlags, IfdStart, IfdSubdirEdge, IfdTable, IfdTag, IfdVariantGroup, - RawConvEffect, + IfdByteOrder, IfdFlags, IfdStart, IfdSubdirEdge, IfdSubdirProcessor, IfdTable, IfdTag, + IfdVariantGroup, RawConvEffect, }; #[allow(unused_imports)] use super::subdir::BaseExpr; #[allow(unused_imports)] +use super::validation::{SizeExpectation, U16SizeCheck}; +#[allow(unused_imports)] use super::{ExprId, Fmt, GateA, Omitted, OtherId, PrintConv, TagGroups}; /// `Image::ExifTool::AIFF::Composite` -- 0 tags, @@ -231,6 +233,8 @@ pub static IFD_APPLE_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -1099,6 +1103,8 @@ pub static IFD_BPG_EXTENSIONS: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: Some("ProcessProc Image::ExifTool::ProcessTIFF"), }), }, @@ -1133,6 +1139,8 @@ pub static IFD_BPG_EXTENSIONS: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -1167,6 +1175,8 @@ pub static IFD_BPG_EXTENSIONS: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -1348,6 +1358,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -1382,6 +1394,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -1438,6 +1452,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -1472,6 +1488,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -1581,6 +1599,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -2140,6 +2160,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -2178,6 +2200,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Serial, unwalked: None, }), }, @@ -2276,6 +2300,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -2368,6 +2394,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -2402,6 +2430,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -2439,6 +2469,18 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: Some(U16SizeCheck { + offset: 0, + expected: &[SizeExpectation::Relative(0)], + expression: "Image::ExifTool::Canon::Validate($dirData,$subdirStart,$size)", + callee: "Image::ExifTool::Canon::Validate", + source_file: "Image/ExifTool/Canon.pm", + source_sha256: "d22b110e7b2e1af2d6e59bb97db97a04ffc2bfd1ba7d078d380ae60fbc70bbba", + reader_contract_sha256: Some( + "20f54dc782fc565a7baad55b5ad0b6cc359b57051d67aa09fd64eb850fee7935", + ), + }), + processor: IfdSubdirProcessor::Serial, unwalked: None, }), }, @@ -2476,6 +2518,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -2536,6 +2580,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -2570,6 +2616,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -2604,6 +2652,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -2667,6 +2717,18 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: Some(U16SizeCheck { + offset: 0, + expected: &[SizeExpectation::Relative(0)], + expression: "Image::ExifTool::Canon::Validate($dirData,$subdirStart,$size)", + callee: "Image::ExifTool::Canon::Validate", + source_file: "Image/ExifTool/Canon.pm", + source_sha256: "d22b110e7b2e1af2d6e59bb97db97a04ffc2bfd1ba7d078d380ae60fbc70bbba", + reader_contract_sha256: Some( + "20f54dc782fc565a7baad55b5ad0b6cc359b57051d67aa09fd64eb850fee7935", + ), + }), + processor: IfdSubdirProcessor::Serial, unwalked: None, }), }, @@ -2731,6 +2793,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -2765,6 +2829,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -2799,6 +2865,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -2833,6 +2901,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -2926,6 +2996,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -2960,6 +3032,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -2994,6 +3068,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -3028,6 +3104,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -3150,6 +3228,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -3184,6 +3264,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -3233,6 +3315,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -3267,6 +3351,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -3360,6 +3446,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -3394,6 +3482,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -3450,6 +3540,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -3565,6 +3657,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -3599,6 +3693,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -3633,6 +3729,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -3667,6 +3765,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -3704,6 +3804,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -3738,6 +3840,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -3772,6 +3876,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -3806,6 +3912,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -3840,6 +3948,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -3874,6 +3984,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -3908,6 +4020,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -3942,6 +4056,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -3976,6 +4092,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -4109,6 +4227,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -4151,6 +4271,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -4193,6 +4315,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -4235,6 +4359,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -4277,6 +4403,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -4319,6 +4447,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -4361,6 +4491,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -4403,6 +4535,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -4445,6 +4579,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -4487,6 +4623,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -4529,6 +4667,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -4571,6 +4711,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -4613,6 +4755,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -4655,6 +4799,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -4697,6 +4843,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -4739,6 +4887,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -4781,6 +4931,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -4823,6 +4975,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -4865,6 +5019,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -4907,6 +5063,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -4949,6 +5107,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -4991,6 +5151,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -5033,6 +5195,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -5075,6 +5239,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -5117,6 +5283,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -5159,6 +5327,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -5201,6 +5371,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -5243,6 +5415,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -5285,6 +5459,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -5327,6 +5503,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -5369,6 +5547,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -5421,6 +5601,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -5491,6 +5673,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -5532,6 +5716,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -5573,6 +5759,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -5610,6 +5798,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -5657,6 +5847,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -5699,6 +5891,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -5741,6 +5935,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -5783,6 +5979,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -5825,6 +6023,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -5867,6 +6067,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -5909,6 +6111,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -5951,6 +6155,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -5993,6 +6199,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -6030,6 +6238,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -6077,6 +6287,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -6147,6 +6359,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -6187,6 +6401,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -6227,6 +6443,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -6315,6 +6533,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -6355,6 +6575,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -6401,6 +6623,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -6459,6 +6683,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -6517,6 +6743,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -6569,6 +6797,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -6615,6 +6845,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -6667,6 +6899,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -6713,6 +6947,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -6750,6 +6986,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -6801,6 +7039,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -6847,6 +7087,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: true, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -6887,6 +7129,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -7359,6 +7603,8 @@ pub static IFD_CASIO_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -7658,6 +7904,8 @@ pub static IFD_CASIO_TYPE2: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -11766,6 +12014,8 @@ pub static IFD_EXIF_MAIN: IfdTable = IfdTable { max_subdirs: Some(1), dir_name: Some("GlobParamIFD"), validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: Some("same-table recursion (TagTable absent)"), }), }, @@ -12142,6 +12392,8 @@ pub static IFD_EXIF_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: Some("XMP"), validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -12314,6 +12566,8 @@ pub static IFD_EXIF_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -14088,6 +14342,8 @@ pub static IFD_EXIF_MAIN: IfdTable = IfdTable { max_subdirs: Some(1), dir_name: Some("KodakIFD"), validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -14410,6 +14666,8 @@ pub static IFD_EXIF_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: Some("IPTC"), validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -14805,6 +15063,8 @@ pub static IFD_EXIF_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: Some("AFCP_IPTC"), validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -14903,6 +15163,8 @@ pub static IFD_EXIF_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: Some("LeafIFD"), validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -14937,6 +15199,8 @@ pub static IFD_EXIF_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: Some("Photoshop"), validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -14975,6 +15239,8 @@ pub static IFD_EXIF_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: Some("ExifIFD"), validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: Some("same-table recursion (TagTable absent)"), }), }, @@ -15009,6 +15275,8 @@ pub static IFD_EXIF_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -15275,6 +15543,8 @@ pub static IFD_EXIF_MAIN: IfdTable = IfdTable { max_subdirs: Some(1), dir_name: Some("GPS"), validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -15567,6 +15837,8 @@ pub static IFD_EXIF_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -16508,6 +16780,8 @@ pub static IFD_EXIF_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -16639,6 +16913,8 @@ pub static IFD_EXIF_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -16870,6 +17146,8 @@ pub static IFD_EXIF_MAIN: IfdTable = IfdTable { max_subdirs: Some(1), dir_name: Some("InteropIFD"), validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: Some("same-table recursion (TagTable absent)"), }), }, @@ -18372,6 +18650,8 @@ pub static IFD_EXIF_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -18406,6 +18686,8 @@ pub static IFD_EXIF_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: Some("XML"), validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -18440,6 +18722,8 @@ pub static IFD_EXIF_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: Some("ProcessProc Image::ExifTool::ProcessSubTIFF"), }), }, @@ -19564,6 +19848,8 @@ pub static IFD_EXIF_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -19649,6 +19935,8 @@ pub static IFD_EXIF_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: Some("AsShotICCProfile"), validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -19712,6 +20000,8 @@ pub static IFD_EXIF_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: Some("CurrentICCProfile"), validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -19915,6 +20205,8 @@ pub static IFD_EXIF_MAIN: IfdTable = IfdTable { max_subdirs: Some(10), dir_name: Some("ProfileIFD"), validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: Some( "same-table recursion (TagTable absent); ProcessProc Image::ExifTool::Exif::ProcessTiffIFD; unmodeled SubDirectory key(s) Magic", ), @@ -20985,6 +21277,8 @@ pub static IFD_EXIF_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -21548,6 +21842,8 @@ pub static IFD_EXIF_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: Some("JUMBF"), validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -21604,6 +21900,8 @@ pub static IFD_EXIF_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -21660,6 +21958,8 @@ pub static IFD_EXIF_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -21761,6 +22061,8 @@ pub static IFD_EXIF_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -21942,6 +22244,8 @@ pub static IFD_EXIF_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: Some("KDC_IFD"), validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -22310,6 +22614,8 @@ pub static IFD_EXIF_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: Some("SR2Private"), validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -22357,6 +22663,8 @@ pub static IFD_EXIF_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -22412,6 +22720,8 @@ pub static IFD_EXIF_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -22459,6 +22769,8 @@ pub static IFD_EXIF_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -22506,6 +22818,8 @@ pub static IFD_EXIF_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -22553,6 +22867,8 @@ pub static IFD_EXIF_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -23313,6 +23629,8 @@ pub static IFD_FLAC_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -23391,6 +23709,8 @@ pub static IFD_FLAC_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -23447,6 +23767,8 @@ pub static IFD_FLAC_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -23620,6 +23942,8 @@ pub static IFD_FLIR_AFF: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -23654,6 +23978,8 @@ pub static IFD_FLIR_AFF: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -23869,6 +24195,8 @@ pub static IFD_FLASH_FLV: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -23903,6 +24231,8 @@ pub static IFD_FLASH_FLV: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -23937,6 +24267,8 @@ pub static IFD_FLASH_FLV: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -24008,6 +24340,8 @@ pub static IFD_FLASH_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -24522,6 +24856,8 @@ pub static IFD_FUJIFILM_IFD: IfdTable = IfdTable { max_subdirs: None, dir_name: Some("FujiSubIFD"), validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -25167,6 +25503,8 @@ pub static IFD_FUJIFILM_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -25201,6 +25539,8 @@ pub static IFD_FUJIFILM_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -25235,6 +25575,8 @@ pub static IFD_FUJIFILM_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -25694,6 +26036,8 @@ pub static IFD_FUJIFILM_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -26624,6 +26968,8 @@ pub static IFD_FUJIFILM_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -26904,6 +27250,8 @@ pub static IFD_GIMP_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -26953,6 +27301,8 @@ pub static IFD_GIMP_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -37727,6 +38077,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -37768,6 +38120,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -37809,6 +38163,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -37850,6 +38206,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -37891,6 +38249,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -37932,6 +38292,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -37973,6 +38335,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -38014,6 +38378,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -38055,6 +38421,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -38096,6 +38464,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -38137,6 +38507,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -38178,6 +38550,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -38219,6 +38593,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -38260,6 +38636,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -38301,6 +38679,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -38342,6 +38722,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -38383,6 +38765,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -38417,6 +38801,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -38451,6 +38837,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -38485,6 +38873,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -38526,6 +38916,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -38567,6 +38959,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -38608,6 +39002,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -38649,6 +39045,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -38690,6 +39088,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -38731,6 +39131,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -38772,6 +39174,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -38813,6 +39217,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -38854,6 +39260,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -38888,6 +39296,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -38929,6 +39339,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -38970,6 +39382,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -39011,6 +39425,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -39052,6 +39468,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -39093,6 +39511,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -39134,6 +39554,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -39175,6 +39597,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -39216,6 +39640,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -39257,6 +39683,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -39298,6 +39726,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -39339,6 +39769,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -39380,6 +39812,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -39421,6 +39855,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -39462,6 +39898,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -39503,6 +39941,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -39544,6 +39984,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -39585,6 +40027,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -39626,6 +40070,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -39667,6 +40113,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -39708,6 +40156,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -39749,6 +40199,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -39790,6 +40242,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -39853,6 +40307,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -39894,6 +40350,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -39935,6 +40393,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -39976,6 +40436,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -40010,6 +40472,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -40051,6 +40515,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -40092,6 +40558,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -40133,6 +40601,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -40174,6 +40644,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -40215,6 +40687,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -40249,6 +40723,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -40290,6 +40766,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -40331,6 +40809,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -40372,6 +40852,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -40413,6 +40895,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -40454,6 +40938,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -40495,6 +40981,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -40529,6 +41017,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -40570,6 +41060,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -40611,6 +41103,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -40652,6 +41146,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -40693,6 +41189,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -40727,6 +41225,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -40768,6 +41268,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -40809,6 +41311,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -40850,6 +41354,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -40891,6 +41397,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -40932,6 +41440,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -40973,6 +41483,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -41014,6 +41526,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -41055,6 +41569,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -41096,6 +41612,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -41137,6 +41655,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -41178,6 +41698,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -41219,6 +41741,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -41260,6 +41784,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -41301,6 +41827,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -41342,6 +41870,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -41383,6 +41913,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -41424,6 +41956,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -41465,6 +41999,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -41506,6 +42042,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -41547,6 +42085,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -41588,6 +42128,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -41629,6 +42171,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -41670,6 +42214,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -41711,6 +42257,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -41752,6 +42300,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -41793,6 +42343,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -41834,6 +42386,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -41875,6 +42429,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -41916,6 +42472,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -41957,6 +42515,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -41998,6 +42558,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -42039,6 +42601,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -42080,6 +42644,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -42121,6 +42687,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -42162,6 +42730,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -42203,6 +42773,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -42244,6 +42816,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -42285,6 +42859,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -42326,6 +42902,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -42367,6 +42945,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -42408,6 +42988,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -42449,6 +43031,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -42490,6 +43074,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -42531,6 +43117,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -42572,6 +43160,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -42606,6 +43196,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -42647,6 +43239,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -42688,6 +43282,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -42729,6 +43325,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -42770,6 +43368,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -42811,6 +43411,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -42852,6 +43454,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -42893,6 +43497,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -42934,6 +43540,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -42975,6 +43583,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -43016,6 +43626,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -43057,6 +43669,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -43098,6 +43712,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -43132,6 +43748,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -43173,6 +43791,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -43214,6 +43834,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -43255,6 +43877,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -43289,6 +43913,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -43330,6 +43956,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -43371,6 +43999,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -43412,6 +44042,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -43453,6 +44085,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -43494,6 +44128,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -43535,6 +44171,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -43576,6 +44214,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -43617,6 +44257,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -43658,6 +44300,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -43699,6 +44343,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -43740,6 +44386,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -43781,6 +44429,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -43822,6 +44472,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -43863,6 +44515,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -43904,6 +44558,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -43945,6 +44601,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -43986,6 +44644,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -44027,6 +44687,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -44068,6 +44730,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -44109,6 +44773,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -44150,6 +44816,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -44191,6 +44859,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -44232,6 +44902,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -44273,6 +44945,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -44314,6 +44988,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -44355,6 +45031,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -44396,6 +45074,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -44437,6 +45117,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -44478,6 +45160,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -44519,6 +45203,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -44560,6 +45246,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -44601,6 +45289,8 @@ pub static IFD_GARMIN_FIT: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -80949,6 +81639,8 @@ pub static IFD_HP_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }], @@ -82406,6 +83098,8 @@ pub static IFD_ISO_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -82440,6 +83134,8 @@ pub static IFD_ISO_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -82508,6 +83204,8 @@ pub static IFD_JPEG_EPPIM: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }], @@ -83283,6 +83981,8 @@ pub static IFD_KODAK_IFD: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -94354,6 +95054,8 @@ pub static IFD_KODAK_IFD: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -95710,6 +96412,8 @@ pub static IFD_KODAK_IFD: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -96392,6 +97096,8 @@ pub static IFD_KODAK_META: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -96430,6 +97136,8 @@ pub static IFD_KODAK_META: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -97315,6 +98023,8 @@ pub static IFD_KODAK_TYPE8: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -97353,6 +98063,8 @@ pub static IFD_KODAK_TYPE8: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -97400,6 +98112,8 @@ pub static IFD_KODAK_TYPE8: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: Some("ProcessProc Image::ExifTool::Kodak::ProcessKodakIFD"), }), }, @@ -97441,6 +98155,8 @@ pub static IFD_KODAK_TYPE8: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -97797,6 +98513,8 @@ pub static IFD_MPEG_XING: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -97896,6 +98614,8 @@ pub static IFD_MPF_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: Some("ProcessProc Image::ExifTool::MPF::ProcessMPImageList"), }), }, @@ -98309,6 +99029,8 @@ pub static IFD_MACOS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -98343,6 +99065,8 @@ pub static IFD_MACOS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: Some("ProcessProc Image::ExifTool::MacOS::ProcessATTR"), }), }, @@ -98417,6 +99141,8 @@ pub static IFD_MATROSKA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -98451,6 +99177,8 @@ pub static IFD_MATROSKA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -98485,6 +99213,8 @@ pub static IFD_MATROSKA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -98519,6 +99249,8 @@ pub static IFD_MATROSKA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -98663,6 +99395,8 @@ pub static IFD_MATROSKA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -98697,6 +99431,8 @@ pub static IFD_MATROSKA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -98772,6 +99508,8 @@ pub static IFD_MATROSKA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -98806,6 +99544,8 @@ pub static IFD_MATROSKA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -98840,6 +99580,8 @@ pub static IFD_MATROSKA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -98874,6 +99616,8 @@ pub static IFD_MATROSKA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -98908,6 +99652,8 @@ pub static IFD_MATROSKA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -98942,6 +99688,8 @@ pub static IFD_MATROSKA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -98976,6 +99724,8 @@ pub static IFD_MATROSKA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -99209,6 +99959,8 @@ pub static IFD_MATROSKA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -99419,6 +100171,8 @@ pub static IFD_MATROSKA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -99453,6 +100207,8 @@ pub static IFD_MATROSKA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -99487,6 +100243,8 @@ pub static IFD_MATROSKA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -99550,6 +100308,8 @@ pub static IFD_MATROSKA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -99584,6 +100344,8 @@ pub static IFD_MATROSKA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -99618,6 +100380,8 @@ pub static IFD_MATROSKA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -99674,6 +100438,8 @@ pub static IFD_MATROSKA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -99833,6 +100599,8 @@ pub static IFD_MATROSKA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -99918,6 +100686,8 @@ pub static IFD_MATROSKA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -99952,6 +100722,8 @@ pub static IFD_MATROSKA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -99986,6 +100758,8 @@ pub static IFD_MATROSKA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -100042,6 +100816,8 @@ pub static IFD_MATROSKA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -100127,6 +100903,8 @@ pub static IFD_MATROSKA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -100219,6 +100997,8 @@ pub static IFD_MATROSKA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -100355,6 +101135,8 @@ pub static IFD_MATROSKA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -100434,6 +101216,8 @@ pub static IFD_MATROSKA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -100468,6 +101252,8 @@ pub static IFD_MATROSKA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -100546,6 +101332,8 @@ pub static IFD_MATROSKA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -100732,6 +101520,8 @@ pub static IFD_MATROSKA_PROJECTION: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -100773,6 +101563,8 @@ pub static IFD_MATROSKA_PROJECTION: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -100937,6 +101729,8 @@ pub static IFD_MINOLTA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -100975,6 +101769,8 @@ pub static IFD_MINOLTA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -101009,6 +101805,8 @@ pub static IFD_MINOLTA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -101047,6 +101845,8 @@ pub static IFD_MINOLTA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -101085,6 +101885,8 @@ pub static IFD_MINOLTA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -101405,6 +102207,8 @@ pub static IFD_MINOLTA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -101472,6 +102276,8 @@ pub static IFD_MINOLTA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -101719,6 +102525,8 @@ pub static IFD_MINOLTA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -101760,6 +102568,8 @@ pub static IFD_MINOLTA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -104689,6 +105499,8 @@ pub static IFD_NIKON_NEFINFO: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -104723,6 +105535,8 @@ pub static IFD_NIKON_NEFINFO: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -105072,6 +105886,8 @@ pub static IFD_NIKON_SCAN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -105106,6 +105922,8 @@ pub static IFD_NIKON_SCAN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -105428,6 +106246,8 @@ pub static IFD_NINTENDO_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }], @@ -105817,6 +106637,8 @@ pub static IFD_OLYMPUS_CAMERASETTINGS: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -105851,6 +106673,8 @@ pub static IFD_OLYMPUS_CAMERASETTINGS: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -108173,6 +108997,8 @@ pub static IFD_OLYMPUS_FOCUSINFO: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -109708,6 +110534,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -109742,6 +110570,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -110315,6 +111145,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -110608,6 +111440,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -111761,6 +112595,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -111802,6 +112638,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -111853,6 +112691,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -111894,6 +112734,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -111945,6 +112787,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -111986,6 +112830,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -112037,6 +112883,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -112078,6 +112926,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -112129,6 +112979,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -112170,6 +113022,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -112227,6 +113081,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -112271,6 +113127,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -112347,6 +113205,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -112388,6 +113248,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -112439,6 +113301,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -112480,6 +113344,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -112531,6 +113397,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -112572,6 +113440,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -112623,6 +113493,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -112664,6 +113536,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -112715,6 +113589,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -112756,6 +113632,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -112807,6 +113685,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -112848,6 +113728,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -112899,6 +113781,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -112940,6 +113824,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -112991,6 +113877,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -113032,6 +113920,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -113083,6 +113973,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -113124,6 +114016,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -113175,6 +114069,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -113216,6 +114112,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -113267,6 +114165,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -113308,6 +114208,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -113359,6 +114261,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -113400,6 +114304,8 @@ pub static IFD_OLYMPUS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -114153,6 +115059,8 @@ pub static IFD_OLYMPUS_RAWDEVELOPMENT2: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -115924,6 +116832,8 @@ pub static IFD_PSP_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -115958,6 +116868,8 @@ pub static IFD_PSP_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -116027,6 +116939,8 @@ pub static IFD_PSP_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -116418,6 +117332,8 @@ pub static IFD_PANASONIC_LEICA3: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -116484,6 +117400,8 @@ pub static IFD_PANASONIC_LEICA4: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -116518,6 +117436,8 @@ pub static IFD_PANASONIC_LEICA4: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -116552,6 +117472,8 @@ pub static IFD_PANASONIC_LEICA4: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -116586,6 +117508,8 @@ pub static IFD_PANASONIC_LEICA4: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -116700,6 +117624,8 @@ pub static IFD_PANASONIC_LEICA5: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -116755,6 +117681,8 @@ pub static IFD_PANASONIC_LEICA5: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -116844,6 +117772,8 @@ pub static IFD_PANASONIC_LEICA5: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: Some("ProcessProc Image::ExifTool::ProcessTIFF"), }), }, @@ -118240,6 +119170,8 @@ pub static IFD_PANASONIC_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -118420,6 +119352,8 @@ pub static IFD_PANASONIC_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -119611,6 +120545,8 @@ pub static IFD_PANASONIC_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -119645,6 +120581,8 @@ pub static IFD_PANASONIC_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -120358,6 +121296,8 @@ pub static IFD_PANASONIC_SUBDIR: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -120392,6 +121332,8 @@ pub static IFD_PANASONIC_SUBDIR: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -121826,6 +122768,8 @@ pub static IFD_PANASONICRAW_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -122032,6 +122976,8 @@ pub static IFD_PANASONICRAW_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -122254,6 +123200,8 @@ pub static IFD_PANASONICRAW_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -122303,6 +123251,8 @@ pub static IFD_PANASONICRAW_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: Some("ProcessProc Image::ExifTool::ProcessTIFF"), }), }, @@ -122385,6 +123335,8 @@ pub static IFD_PANASONICRAW_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: Some("XMP"), validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -122452,6 +123404,8 @@ pub static IFD_PANASONICRAW_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: Some("IPTC"), validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -122490,6 +123444,8 @@ pub static IFD_PANASONICRAW_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: Some("ExifIFD"), validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -122528,6 +123484,8 @@ pub static IFD_PANASONICRAW_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: Some("GPS"), validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -123921,6 +124879,8 @@ pub static IFD_PENTAX_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -124294,6 +125254,8 @@ pub static IFD_PENTAX_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -124395,6 +125357,8 @@ pub static IFD_PENTAX_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -124451,6 +125415,8 @@ pub static IFD_PENTAX_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -124792,6 +125758,8 @@ pub static IFD_PENTAX_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -125304,6 +126272,8 @@ pub static IFD_PENTAX_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -125338,6 +126308,8 @@ pub static IFD_PENTAX_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -125424,6 +126396,8 @@ pub static IFD_PENTAX_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -125480,6 +126454,8 @@ pub static IFD_PENTAX_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -125514,6 +126490,8 @@ pub static IFD_PENTAX_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -125548,6 +126526,8 @@ pub static IFD_PENTAX_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -125582,6 +126562,8 @@ pub static IFD_PENTAX_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -125619,6 +126601,8 @@ pub static IFD_PENTAX_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -125656,6 +126640,8 @@ pub static IFD_PENTAX_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -125708,6 +126694,8 @@ pub static IFD_PENTAX_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -125825,6 +126813,8 @@ pub static IFD_PENTAX_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -125859,6 +126849,8 @@ pub static IFD_PENTAX_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -125908,6 +126900,8 @@ pub static IFD_PENTAX_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -125942,6 +126936,8 @@ pub static IFD_PENTAX_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -126064,6 +127060,8 @@ pub static IFD_PENTAX_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -126098,6 +127096,8 @@ pub static IFD_PENTAX_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -126132,6 +127132,8 @@ pub static IFD_PENTAX_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -126646,6 +127648,8 @@ pub static IFD_PENTAX_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -126683,6 +127687,8 @@ pub static IFD_PENTAX_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -126779,6 +127785,8 @@ pub static IFD_PENTAX_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -126816,6 +127824,8 @@ pub static IFD_PENTAX_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -126861,6 +127871,8 @@ pub static IFD_PENTAX_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -126898,6 +127910,8 @@ pub static IFD_PENTAX_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -126945,6 +127959,8 @@ pub static IFD_PENTAX_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -126982,6 +127998,8 @@ pub static IFD_PENTAX_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -127029,6 +128047,8 @@ pub static IFD_PENTAX_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -127066,6 +128086,8 @@ pub static IFD_PENTAX_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -127113,6 +128135,8 @@ pub static IFD_PENTAX_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -127150,6 +128174,8 @@ pub static IFD_PENTAX_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -127429,6 +128455,8 @@ pub static IFD_PENTAX_TYPE2: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -128960,6 +129988,8 @@ pub static IFD_RICOH_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -129637,6 +130667,8 @@ pub static IFD_RICOH_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -129681,6 +130713,8 @@ pub static IFD_RICOH_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -129951,6 +130985,8 @@ pub static IFD_RICOH_SUBDIR: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -129985,6 +131021,8 @@ pub static IFD_RICOH_SUBDIR: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -130034,6 +131072,8 @@ pub static IFD_RICOH_SUBDIR: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -130352,6 +131392,8 @@ pub static IFD_SAMSUNG_TYPE2: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -130386,6 +131428,8 @@ pub static IFD_SAMSUNG_TYPE2: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -131527,6 +132571,8 @@ pub static IFD_SAMSUNG_TYPE2: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -131572,6 +132618,8 @@ pub static IFD_SAMSUNG_TYPE2: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -132089,6 +133137,8 @@ pub static IFD_SANYO_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -132172,6 +133222,8 @@ pub static IFD_SANYO_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -133248,6 +134300,8 @@ pub static IFD_SIGMA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -133282,6 +134336,8 @@ pub static IFD_SIGMA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -134401,6 +135457,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -134489,6 +135547,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -135046,6 +136106,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -135338,6 +136400,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -135407,6 +136471,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -135444,6 +136510,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -135478,6 +136546,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -135512,6 +136582,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -135749,6 +136821,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -136133,6 +137207,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -136699,6 +137775,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -136745,6 +137823,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -136785,6 +137865,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -136822,6 +137904,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -136873,6 +137957,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -136910,6 +137996,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -136961,6 +138049,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -137001,6 +138091,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -137047,6 +138139,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -137084,6 +138178,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -137131,6 +138227,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -137173,6 +138271,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -137210,6 +138310,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -137257,6 +138359,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -137299,6 +138403,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -137341,6 +138447,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -137389,6 +138497,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -137445,6 +138555,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -137487,6 +138599,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -137529,6 +138643,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -137571,6 +138687,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -137613,6 +138731,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -138309,6 +139429,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -138351,6 +139473,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -138393,6 +139517,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -138449,6 +139575,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -138534,6 +139662,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -138611,6 +139741,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -138651,6 +139783,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -138730,6 +139864,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -138809,6 +139945,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -138888,6 +140026,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -138930,6 +140070,8 @@ pub static IFD_SONY_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -139485,6 +140627,8 @@ pub static IFD_SONY_SR2SUBIFD: IfdTable = IfdTable { max_subdirs: Some(20), dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -144560,6 +145704,8 @@ pub static IFD_STIM_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -144594,6 +145740,8 @@ pub static IFD_STIM_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -144910,6 +146058,8 @@ pub static IFD_THEORA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -144944,6 +146094,8 @@ pub static IFD_THEORA_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -145120,6 +146272,8 @@ pub static IFD_UNKNOWN_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }], @@ -145242,6 +146396,8 @@ pub static IFD_VORBIS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, @@ -145276,6 +146432,8 @@ pub static IFD_VORBIS_MAIN: IfdTable = IfdTable { max_subdirs: None, dir_name: None, validate: false, + validation: None, + processor: IfdSubdirProcessor::Native, unwalked: None, }), }, From fbacf2b3be27afe5e794af1809d6e984468633b4 Mon Sep 17 00:00:00 2001 From: swackhamer Date: Sun, 13 Sep 2026 09:59:17 -0500 Subject: [PATCH 05/12] test: cover generated Canon AFInfo2 public route --- tests/canon_afinfo2_generated.rs | 366 +++++++++++++++++++++++++++++++ 1 file changed, 366 insertions(+) create mode 100644 tests/canon_afinfo2_generated.rs diff --git a/tests/canon_afinfo2_generated.rs b/tests/canon_afinfo2_generated.rs new file mode 100644 index 000000000..811e54541 --- /dev/null +++ b/tests/canon_afinfo2_generated.rs @@ -0,0 +1,366 @@ +//! Public-reader coverage for the generated `Canon::AFInfo2` serial table. +//! +//! These TIFF carriers are assembled as real TIFF -> ExifIFD -> Canon MakerNote +//! structures so `read_metadata()` takes the normal public path. They do not +//! import the generated table or its descriptors. Expected output was recorded +//! from pinned ExifTool 13.59 / Perl 5.38.2 in +//! `shared-pilot/serial-afinfo-integration-20260913/parent-bridge-contract/`: +//! the `semantic-r2/dynamic-corrected-*` cases prove signed arrays, multiword +//! DecodeBits and both byte orders; the parent bridge controls prove the +//! `Validate` rejection, `AFInfo3` state and continuation to Main 0x0081. +//! +//! The minimal synthetic TIFFs used during that collection cause ExifTool's +//! known MakerNote-offset warning. This test intentionally does not assert +//! warning text: OxiDex's public reader exposes metadata, while the contract +//! here is the generated child route and the parent entries surrounding it. + +use oxidex::core::MetadataMap; +use oxidex::core::operations::read_metadata; +use std::fs; + +#[derive(Clone, Copy, Debug)] +enum Endian { + Ii, + Mm, +} + +impl Endian { + fn marker(self) -> [u8; 2] { + match self { + Self::Ii => *b"II", + Self::Mm => *b"MM", + } + } + + fn u16(self, value: u16) -> [u8; 2] { + match self { + Self::Ii => value.to_le_bytes(), + Self::Mm => value.to_be_bytes(), + } + } + + fn u32(self, value: u32) -> [u8; 4] { + match self { + Self::Ii => value.to_le_bytes(), + Self::Mm => value.to_be_bytes(), + } + } +} + +#[derive(Clone, Copy)] +enum Parent { + AfInfo2, + AfInfo3, +} + +impl Parent { + fn raw_id(self) -> u16 { + match self { + Self::AfInfo2 => 0x0026, + Self::AfInfo3 => 0x003c, + } + } +} + +#[derive(Clone)] +struct Child { + parent: Parent, + /// The TIFF entry count that the parent `U16SizeCheck` compares against + /// the child record's first inherited-order u16. + declared_len: u32, + bytes: Vec, +} + +fn put(bytes: &mut [u8], at: usize, value: &[u8]) { + bytes[at..at + value.len()].copy_from_slice(value); +} + +fn put_u16(bytes: &mut [u8], at: usize, order: Endian, value: u16) { + put(bytes, at, &order.u16(value)); +} + +fn put_u32(bytes: &mut [u8], at: usize, order: Endian, value: u32) { + put(bytes, at, &order.u32(value)); +} + +fn align2(value: usize) -> usize { + (value + 1) & !1 +} + +/// The actual `%Canon::AFInfo2` wire layout: eight fixed u16 slots, then four +/// signed `NumAFPoints` arrays, a ceil(n / 16) bitset, and the native +/// non-EOS unknown tail plus PrimaryAFPoint. `AFInfo3` uses the same child +/// payload but its parent sets state that prevents the final primary field. +fn afinfo2_child(order: Endian, point_count: u16, primary: u16) -> Vec { + let n = usize::from(point_count); + let bit_words = (n + 15) / 16; + let word_count = 8 + (4 * n) + bit_words + (bit_words + 1) + 1; + let size = u16::try_from(word_count * 2).expect("test child stays within u16 size"); + let mut words = Vec::with_capacity(word_count); + words.extend([size, 2, point_count, 1, 100, 80, 50, 40]); + + // A signed high-bit value verifies that child data is decoded as int16s, + // followed by an ordinary positive value. The other three arrays are + // deliberately zeroed, matching the pinned native fixture shape. + for array in 0..4 { + for index in 0..n { + let value = match (array, index) { + (0, 0) => 0x8001, + (0, 1) => 2, + _ => 0, + }; + words.push(value); + } + } + if bit_words > 0 { + words.push(0x8001); // point 0 and point 15 + for _ in 1..bit_words { + words.push(1); // point 16 for the n=17 control + } + } + // On non-EOS bodies key 13 is an Unknown field, but ProcessSerialData + // still consumes its ceil(n/16)+1 words before key 14. + for _ in 0..bit_words + 1 { + words.push(0); + } + words.push(primary); + + let mut bytes = Vec::with_capacity(words.len() * 2); + for word in words { + bytes.extend_from_slice(&order.u16(word)); + } + bytes +} + +fn invalid_size_child(order: Endian) -> Child { + let mut bytes = afinfo2_child(order, 0, 99); + // `Validate($dirData, $subdirStart, $size)` must see the declared 20-byte + // child length. This 19 makes the validation false before table lookup. + put_u16(&mut bytes, 0, order, 19); + Child { + parent: Parent::AfInfo2, + declared_len: u32::try_from(bytes.len()).expect("fixture length"), + bytes, + } +} + +fn truncated_child(order: Endian) -> Child { + let mut bytes = afinfo2_child(order, 17, 99); + // The parent advertises the complete 164-byte record, but the carrier + // ends after twenty words. Pinned ExifTool warns while continuing Main. + bytes.truncate(40); + Child { + parent: Parent::AfInfo2, + declared_len: 164, + bytes, + } +} + +fn child(parent: Parent, order: Endian, point_count: u16) -> Child { + let bytes = afinfo2_child(order, point_count, 99); + Child { + parent, + declared_len: u32::try_from(bytes.len()).expect("fixture length"), + bytes, + } +} + +/// Build offsets after the IFDs and their entry arrays have been laid out. +/// Every pointer is absolute from the TIFF start and every child begins after +/// the MakerNote directory, so fixture data never overlaps an IFD entry. +fn canon_tiff(order: Endian, model: &str, children: &[Child], later_sibling: bool) -> Vec { + const IFD0: usize = 8; + const MAKE_AT: usize = 64; + const MODEL_AT: usize = 80; + const EXIF_IFD: usize = 112; + const MAKERNOTE: usize = 160; + const TIFF_TYPE_ASCII: u16 = 2; + const TIFF_TYPE_LONG: u16 = 4; + const TIFF_TYPE_UNDEFINED: u16 = 7; + + let maker_entries = children.len() + usize::from(later_sibling); + let maker_dir_len = 2 + (maker_entries * 12) + 4; + let mut child_at = align2(MAKERNOTE + maker_dir_len); + let mut bytes = vec![0_u8; child_at]; + + put(&mut bytes, 0, &order.marker()); + put_u16(&mut bytes, 2, order, 42); + put_u32(&mut bytes, 4, order, IFD0 as u32); + + put_u16(&mut bytes, IFD0, order, 3); + let mut entry = IFD0 + 2; + for (tag, format, count, value) in [ + (0x010f, TIFF_TYPE_ASCII, 6_u32, MAKE_AT as u32), + ( + 0x0110, + TIFF_TYPE_ASCII, + u32::try_from(model.len() + 1).expect("model length"), + MODEL_AT as u32, + ), + (0x8769, TIFF_TYPE_LONG, 1_u32, EXIF_IFD as u32), + ] { + put_u16(&mut bytes, entry, order, tag); + put_u16(&mut bytes, entry + 2, order, format); + put_u32(&mut bytes, entry + 4, order, count); + put_u32(&mut bytes, entry + 8, order, value); + entry += 12; + } + put_u32(&mut bytes, entry, order, 0); + put(&mut bytes, MAKE_AT, b"Canon\0"); + put(&mut bytes, MODEL_AT, model.as_bytes()); + bytes[MODEL_AT + model.len()] = 0; + + put_u16(&mut bytes, EXIF_IFD, order, 1); + put_u16(&mut bytes, EXIF_IFD + 2, order, 0x927c); + put_u16(&mut bytes, EXIF_IFD + 4, order, TIFF_TYPE_UNDEFINED); + put_u32( + &mut bytes, + EXIF_IFD + 6, + order, + u32::try_from(maker_dir_len).expect("maker directory length") + + children + .iter() + .map(|child| u32::try_from(child.bytes.len()).expect("child length")) + .sum::(), + ); + put_u32(&mut bytes, EXIF_IFD + 10, order, MAKERNOTE as u32); + put_u32(&mut bytes, EXIF_IFD + 14, order, 0); + + put_u16( + &mut bytes, + MAKERNOTE, + order, + u16::try_from(maker_entries).expect("maker entry count"), + ); + entry = MAKERNOTE + 2; + for child in children { + put_u16(&mut bytes, entry, order, child.parent.raw_id()); + put_u16(&mut bytes, entry + 2, order, TIFF_TYPE_UNDEFINED); + put_u32(&mut bytes, entry + 4, order, child.declared_len); + put_u32(&mut bytes, entry + 8, order, child_at as u32); + let end = child_at + child.bytes.len(); + if bytes.len() < end { + bytes.resize(end, 0); + } + put(&mut bytes, child_at, &child.bytes); + child_at = align2(end); + entry += 12; + } + if later_sibling { + put_u16(&mut bytes, entry, order, 0x0081); + put_u16(&mut bytes, entry + 2, order, TIFF_TYPE_LONG); + put_u32(&mut bytes, entry + 4, order, 1); + put_u32(&mut bytes, entry + 8, order, 0x1234_5678); + entry += 12; + } + put_u32(&mut bytes, entry, order, 0); + bytes +} + +fn read_carrier(bytes: &[u8]) -> MetadataMap { + let file = tempfile::Builder::new() + .suffix(".tif") + .tempfile() + .expect("create TIFF carrier"); + fs::write(file.path(), bytes).expect("write TIFF carrier"); + read_metadata(file.path()).expect("public metadata reader accepts Canon TIFF carrier") +} + +fn shown(metadata: &MetadataMap, key: &str) -> Option { + let value = metadata.get(key)?; + value + .as_string() + .map(str::to_owned) + .or_else(|| value.as_integer().map(|value| value.to_string())) +} + +fn assert_present(metadata: &MetadataMap, key: &str, expected: &str) { + assert_eq!(shown(metadata, key).as_deref(), Some(expected), "{key}"); +} + +#[test] +fn generated_afinfo2_route_handles_signed_multiword_records_in_both_orders() { + for order in [Endian::Ii, Endian::Mm] { + let metadata = read_carrier(&canon_tiff( + order, + "Canon Test", + &[child(Parent::AfInfo2, order, 17)], + true, + )); + + // Pinned ExifTool's dynamic-corrected controls report these exact + // values. In particular, 0x8001 is signed -32767 and the two bit + // words render points 0, 15 and 16 rather than a numeric mask. + assert_present(&metadata, "Canon:AFAreaMode", "Single-point AF"); + assert_present( + &metadata, + "Canon:AFAreaWidths", + "-32767 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0", + ); + assert_present(&metadata, "Canon:AFPointsInFocus", "0,15,16"); + assert_present(&metadata, "Canon:PrimaryAFPoint", "99"); + assert_present(&metadata, "Canon:RawDataOffset", "305419896"); + } +} + +#[test] +fn afinfo3_state_and_eos_condition_suppress_primary_but_leave_child_output() { + for (parent, model, label) in [ + (Parent::AfInfo3, "Canon Test", "AFInfo3 parent state"), + (Parent::AfInfo2, "Canon EOS Test", "EOS model condition"), + ] { + let metadata = read_carrier(&canon_tiff( + Endian::Ii, + model, + &[child(parent, Endian::Ii, 17)], + true, + )); + assert_present(&metadata, "Canon:AFAreaMode", "Single-point AF"); + assert_present(&metadata, "Canon:AFPointsInFocus", "0,15,16"); + assert!( + shown(&metadata, "Canon:PrimaryAFPoint").is_none(), + "{label} must select no PrimaryAFPoint alternative" + ); + assert_present(&metadata, "Canon:RawDataOffset", "305419896"); + } +} + +#[test] +fn rejected_size_and_zero_count_do_not_prevent_later_main_entries() { + let invalid = read_carrier(&canon_tiff( + Endian::Ii, + "Canon Test", + &[invalid_size_child(Endian::Ii)], + true, + )); + // Native `Validate($dirData, $subdirStart, $size)` rejects this child + // before AFInfo2 selection. It continues the parent IFD, which is why + // the unrelated Main scalar must still be visible. + assert!(shown(&invalid, "Canon:AFAreaMode").is_none()); + assert_present(&invalid, "Canon:RawDataOffset", "305419896"); + + let truncated = read_carrier(&canon_tiff( + Endian::Ii, + "Canon Test", + &[truncated_child(Endian::Ii)], + true, + )); + // The native control warns that it cannot read Main 0x0026, but continues + // to 0x0081. The public reader has no warning channel, so absence plus + // continuation is the observable contract. + assert!(shown(&truncated, "Canon:AFAreaMode").is_none()); + assert_present(&truncated, "Canon:RawDataOffset", "305419896"); + + let zero = read_carrier(&canon_tiff( + Endian::Mm, + "Canon Test", + &[child(Parent::AfInfo2, Endian::Mm, 0)], + true, + )); + // A well-formed zero-count child is 20 bytes: eight fixed fields, no + // arrays/bitset, then the one native non-EOS unknown word and primary. + // It has no AFAreaWidths but its later scalar and parent sibling remain. + assert!(shown(&zero, "Canon:AFAreaWidths").is_none()); + assert_present(&zero, "Canon:PrimaryAFPoint", "99"); + assert_present(&zero, "Canon:RawDataOffset", "305419896"); +} From e77bee34d58835e1e892f573477c95a96ddbec88 Mon Sep 17 00:00:00 2001 From: swackhamer Date: Sun, 13 Sep 2026 10:06:58 -0500 Subject: [PATCH 06/12] test: export exact Canon AFInfo2 carriers --- tests/canon_afinfo2_generated.rs | 122 ++++++++++++++++++++++--------- 1 file changed, 87 insertions(+), 35 deletions(-) diff --git a/tests/canon_afinfo2_generated.rs b/tests/canon_afinfo2_generated.rs index 811e54541..e7e60f951 100644 --- a/tests/canon_afinfo2_generated.rs +++ b/tests/canon_afinfo2_generated.rs @@ -17,6 +17,8 @@ use oxidex::core::MetadataMap; use oxidex::core::operations::read_metadata; use std::fs; +use std::io::Write; +use std::path::Path; #[derive(Clone, Copy, Debug)] enum Endian { @@ -257,7 +259,33 @@ fn canon_tiff(order: Endian, model: &str, children: &[Child], later_sibling: boo bytes } -fn read_carrier(bytes: &[u8]) -> MetadataMap { +/// When `OXIDEX_AFINFO2_TEST_EXPORT_DIR` is set, retain the exact input that +/// the public reader receives for an independent native replay. `create_new` +/// keeps a second test run from silently replacing a reviewed fixture. +fn export_fixture(label: &str, bytes: &[u8]) { + let Some(dir) = std::env::var_os("OXIDEX_AFINFO2_TEST_EXPORT_DIR") else { + return; + }; + let dir = Path::new(&dir); + fs::create_dir_all(dir).expect("create AFInfo2 fixture export directory"); + let path = dir.join(format!("{label}.tif")); + let mut file = fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&path) + .unwrap_or_else(|error| { + panic!( + "export {label} without overwrite at {}: {error}", + path.display() + ) + }); + file.write_all(bytes).unwrap_or_else(|error| { + panic!("write exported AFInfo2 fixture {}: {error}", path.display()) + }); +} + +fn read_carrier(label: &str, bytes: &[u8]) -> MetadataMap { + export_fixture(label, bytes); let file = tempfile::Builder::new() .suffix(".tif") .tempfile() @@ -281,12 +309,19 @@ fn assert_present(metadata: &MetadataMap, key: &str, expected: &str) { #[test] fn generated_afinfo2_route_handles_signed_multiword_records_in_both_orders() { for order in [Endian::Ii, Endian::Mm] { - let metadata = read_carrier(&canon_tiff( - order, - "Canon Test", - &[child(Parent::AfInfo2, order, 17)], - true, - )); + let label = match order { + Endian::Ii => "valid-afinfo2-ii", + Endian::Mm => "valid-afinfo2-mm", + }; + let metadata = read_carrier( + label, + &canon_tiff( + order, + "Canon Test", + &[child(Parent::AfInfo2, order, 17)], + true, + ), + ); // Pinned ExifTool's dynamic-corrected controls report these exact // values. In particular, 0x8001 is signed -32767 and the two bit @@ -305,21 +340,29 @@ fn generated_afinfo2_route_handles_signed_multiword_records_in_both_orders() { #[test] fn afinfo3_state_and_eos_condition_suppress_primary_but_leave_child_output() { - for (parent, model, label) in [ - (Parent::AfInfo3, "Canon Test", "AFInfo3 parent state"), - (Parent::AfInfo2, "Canon EOS Test", "EOS model condition"), + for (parent, model, assertion_label, export_label) in [ + ( + Parent::AfInfo3, + "Canon Test", + "AFInfo3 parent state", + "afinfo3-state-ii", + ), + ( + Parent::AfInfo2, + "Canon EOS Test", + "EOS model condition", + "eos-afinfo2-ii", + ), ] { - let metadata = read_carrier(&canon_tiff( - Endian::Ii, - model, - &[child(parent, Endian::Ii, 17)], - true, - )); + let metadata = read_carrier( + export_label, + &canon_tiff(Endian::Ii, model, &[child(parent, Endian::Ii, 17)], true), + ); assert_present(&metadata, "Canon:AFAreaMode", "Single-point AF"); assert_present(&metadata, "Canon:AFPointsInFocus", "0,15,16"); assert!( shown(&metadata, "Canon:PrimaryAFPoint").is_none(), - "{label} must select no PrimaryAFPoint alternative" + "{assertion_label} must select no PrimaryAFPoint alternative" ); assert_present(&metadata, "Canon:RawDataOffset", "305419896"); } @@ -327,36 +370,45 @@ fn afinfo3_state_and_eos_condition_suppress_primary_but_leave_child_output() { #[test] fn rejected_size_and_zero_count_do_not_prevent_later_main_entries() { - let invalid = read_carrier(&canon_tiff( - Endian::Ii, - "Canon Test", - &[invalid_size_child(Endian::Ii)], - true, - )); + let invalid = read_carrier( + "invalid-size-ii", + &canon_tiff( + Endian::Ii, + "Canon Test", + &[invalid_size_child(Endian::Ii)], + true, + ), + ); // Native `Validate($dirData, $subdirStart, $size)` rejects this child // before AFInfo2 selection. It continues the parent IFD, which is why // the unrelated Main scalar must still be visible. assert!(shown(&invalid, "Canon:AFAreaMode").is_none()); assert_present(&invalid, "Canon:RawDataOffset", "305419896"); - let truncated = read_carrier(&canon_tiff( - Endian::Ii, - "Canon Test", - &[truncated_child(Endian::Ii)], - true, - )); + let truncated = read_carrier( + "truncated-afinfo2-ii", + &canon_tiff( + Endian::Ii, + "Canon Test", + &[truncated_child(Endian::Ii)], + true, + ), + ); // The native control warns that it cannot read Main 0x0026, but continues // to 0x0081. The public reader has no warning channel, so absence plus // continuation is the observable contract. assert!(shown(&truncated, "Canon:AFAreaMode").is_none()); assert_present(&truncated, "Canon:RawDataOffset", "305419896"); - let zero = read_carrier(&canon_tiff( - Endian::Mm, - "Canon Test", - &[child(Parent::AfInfo2, Endian::Mm, 0)], - true, - )); + let zero = read_carrier( + "zero-count-afinfo2-mm", + &canon_tiff( + Endian::Mm, + "Canon Test", + &[child(Parent::AfInfo2, Endian::Mm, 0)], + true, + ), + ); // A well-formed zero-count child is 20 bytes: eight fixed fields, no // arrays/bitset, then the one native non-EOS unknown word and primary. // It has no AFAreaWidths but its later scalar and parent sibling remain. From 3d0353f45ac21b8237bb3a94a52214de18aa2c56 Mon Sep 17 00:00:00 2001 From: swackhamer Date: Sun, 13 Sep 2026 10:08:23 -0500 Subject: [PATCH 07/12] Retire manual Canon AFInfo2 decoding behind generated ownership --- docs/reference/afinfo2-production-plan.md | 32 ++- src/exiftool_tables/enabled_serial.rs | 16 +- src/exiftool_tables/ifd_engine.rs | 179 +++++++++++-- src/exiftool_tables/ifd_tables.rs | 4 +- src/parsers/tiff/makernotes/canon.rs | 245 +----------------- .../tiff/makernotes/canon/main_engine.rs | 11 +- tools/exiftool-tables/codegen.py | 6 +- tools/exiftool-tables/test_codegen_ifd.py | 3 +- 8 files changed, 219 insertions(+), 277 deletions(-) diff --git a/docs/reference/afinfo2-production-plan.md b/docs/reference/afinfo2-production-plan.md index 34b4b94f3..21f4f79ed 100644 --- a/docs/reference/afinfo2-production-plan.md +++ b/docs/reference/afinfo2-production-plan.md @@ -70,7 +70,31 @@ the complete migration and its gates. ## Current state -Source checkpoints add the generic schema/compiler and parent bridge, plus an -independent live validation oracle. Integration and retirement are in progress. -No Canon manual reader is counted as retired yet. No project-wide generated -percentage is inferred from these two edges or this table's 16 alternatives. +The source checkpoint is published on +`codex/afinfo2-production-integration-20260913`. Integration removes the manual +AFInfo2/AFInfo3 arm, eight private sequence offsets, two parent-ID constants and +the private 20-value AFAreaMode enum. The shared reader is the sole replacement. +An independent source review accepts the ownership and rollback design. + +At the earlier `757b861e` checkpoint, a fresh native/control/candidate pair +covered 53 real and 14 constructed files: 12,758 oracle tags, no process or +parse failures, correct rows 11,841 -> 11,843 and extras 43 -> 3. All 53 real +outputs were unchanged. Per-file matched-key sets lost no correct row; changes +were native rejection of invalid-size/zero-prefix children and two zero-count +PrimaryAFPoint corrections. This checkpoint still contained the fallback arm, +so its pair is not final retirement evidence. + +With the manual arm removed, three public-reader tests pass, covering both +byte orders, signed/multiword data, EOS/AFInfo3 state, invalid size, truncation, +zero count and later siblings. The first shared-test compile exposed a missing +test-only Ctx import; after repair, all 55 IFD-reader tests pass. All 61 IFD +codegen tests and full Clippy pass. Retain that failed attempt in the evidence. + +Independent artifact review also found that supported processor selection was +not yet verified and that old AFInfo's unsupported geometry validator was +incorrectly emitted as executable. The generator now leaves that distinct +edge explicitly unwalked. The processor-oracle repair, full official +regeneration, exact exported-fixture oracle checks, the pair after retirement, +upgrade proof, full corpus and final hosted acceptance remain required before +merge. No Canon reader is counted as merged retirement yet; no project-wide +percentage follows from this bounded work. diff --git a/src/exiftool_tables/enabled_serial.rs b/src/exiftool_tables/enabled_serial.rs index c41edf461..4f629f8e3 100644 --- a/src/exiftool_tables/enabled_serial.rs +++ b/src/exiftool_tables/enabled_serial.rs @@ -15,10 +15,18 @@ pub static ENABLED_SERIAL: &[(&str, &str)] = &[("Canon", "AFInfo2")]; /// Gate B plus the generated table's Gate A. #[must_use] pub fn is_enabled(table: &SerialTable) -> bool { - table.gate_a.passes() - && ENABLED_SERIAL - .binary_search_by(|(module, name)| (*module, *name).cmp(&(table.module, table.table))) - .is_ok() + table.gate_a.passes() && owns(table.module, table.table) +} + +/// The migration decision is independent of current source representability. +/// A later refused definition must not reactivate a retired manual producer. +#[must_use] +pub fn owns(module: &str, table: &str) -> bool { + ENABLED_SERIAL + .binary_search_by(|(candidate_module, name)| { + (*candidate_module, *name).cmp(&(module, table)) + }) + .is_ok() } #[cfg(test)] diff --git a/src/exiftool_tables/ifd_engine.rs b/src/exiftool_tables/ifd_engine.rs index 5faf52f96..a7d83d299 100644 --- a/src/exiftool_tables/ifd_engine.rs +++ b/src/exiftool_tables/ifd_engine.rs @@ -782,14 +782,14 @@ pub enum EntryRead { /// /// `Handled` includes native no-output paths such as a false parent condition, /// a failed source-authenticated validator, an empty child, and a selected -/// serial record that ends at a normal unmatched alternative. `Fallback` is -/// reserved for OxiDex's inability to authenticate or execute the generated -/// route (Gate A/B, missing table/proof, or tainted serial walk). A carrier -/// with a legacy reader can therefore retain its prior producer only in the -/// latter case, without converting a native omission into invented output. +/// serial record that ends at a normal unmatched alternative. `Refused` is +/// an enabled route whose execution could not be proved; it publishes no +/// speculative child rows or state. `Fallback` belongs only to a table whose +/// ownership has not transferred to the shared reader. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum SerialSubdirRead { Handled, + Refused, Fallback, } @@ -1455,8 +1455,8 @@ impl SerialEmissionSink for IfdSerialSink<'_> { /// Publish a serial child's buffered rows only when its entire native walk /// stayed authenticated. A later unsupported state action invalidates an -/// earlier prefix as well: callers must take their documented fallback rather -/// than mixing two producers for one parent entry. +/// earlier prefix as well. The ownership wrapper turns this internal fallback +/// into an explicit refusal and restores the edge's prior state and output. fn finish_serial_child( out: &mut Vec, child: Vec, @@ -1481,10 +1481,30 @@ fn direct_serial_no_match(table: &'static IfdTable, id: u16) -> bool { }; tag.condition.is_some() && !tag.omitted.condition - && matches!( - tag.subdir.as_ref().map(|edge| edge.processor), - Some(IfdSubdirProcessor::Serial) - ) + && tag.subdir.as_ref().is_some_and(|edge| { + edge.processor == IfdSubdirProcessor::Serial + && enabled_serial::owns(edge.module, edge.table) + }) +} + +/// An enabled edge owns its output even when later source/execution facts are +/// refused. Parent effects were applied before descent, so the snapshot keeps +/// them while discarding any speculative child effects and earlier child rows. +fn owned_serial_attempt( + ctx: &mut cond::Ctx, + out: &mut Vec, + attempt: impl FnOnce(&mut cond::Ctx, &mut Vec) -> DescendOutcome, +) -> DescendOutcome { + let members = ctx.members.clone(); + let before = out.len(); + let outcome = attempt(ctx, out); + if outcome == DescendOutcome::Serial(SerialSubdirRead::Fallback) { + *ctx.members = members; + out.truncate(before); + DescendOutcome::Serial(SerialSubdirRead::Refused) + } else { + outcome + } } /// Exif.pm:6919-7102 -- open the directory (or directories) an entry points @@ -1499,6 +1519,28 @@ fn descend( ctx: &mut cond::Ctx, guard: &mut Guard, out: &mut Vec, +) -> DescendOutcome { + if edge.processor != IfdSubdirProcessor::Serial { + return descend_inner(table, tag, edge, located, dir, ctx, guard, out); + } + if !enabled_serial::owns(edge.module, edge.table) { + return DescendOutcome::Serial(SerialSubdirRead::Fallback); + } + owned_serial_attempt(ctx, out, |ctx, out| { + descend_inner(table, tag, edge, located, dir, ctx, guard, out) + }) +} + +#[allow(clippy::too_many_arguments)] +fn descend_inner( + table: &'static IfdTable, + tag: &'static IfdTag, + edge: &IfdSubdirEdge, + located: &Located<'_>, + dir: &IfdDir<'_>, + ctx: &mut cond::Ctx, + guard: &mut Guard, + out: &mut Vec, ) -> DescendOutcome { // Legacy IFD/binary Validate remains unwalked. A serial edge may proceed // only when codegen carried the independently authenticated primitive; @@ -1768,19 +1810,10 @@ fn descend( serial_outcome = SerialSubdirRead::Handled; continue; } - // A serial state refusal may occur after an earlier selected - // row. Its result is a legacy fallback, so do not expose a - // prefix from the unauthenticated walk beside the hand - // producer. Commit the child rows only after the whole - // directory is known clean. + // Commit child rows only after the entire serial directory + // is proved. The outer edge transaction also owns state + // rollback, including earlier child iterations. let mut serial_rows = Vec::new(); - // `process_serial_directory` may set members before reaching - // a later refusal. Its caller must not then enter the legacy - // producer with a prefix of state that native never proved - // this shared route could execute. Parent selection effects - // (for example AFInfo3's condition assignment) happened - // before this snapshot and are intentionally retained. - let members_before = ctx.members.clone(); guard.depth += 1; let mut sink = IfdSerialSink { out: &mut serial_rows, @@ -1801,7 +1834,9 @@ fn descend( guard.depth -= 1; serial_outcome = finish_serial_child(out, serial_rows, &result); if serial_outcome == SerialSubdirRead::Fallback { - *ctx.members = members_before; + // Refuse the whole edge, including prior child iterations; + // the ownership wrapper rolls back their state and rows. + return DescendOutcome::Serial(SerialSubdirRead::Fallback); } } } @@ -1818,7 +1853,7 @@ mod tests { use std::collections::HashMap; use super::*; - use crate::exiftool_tables::cond::{CmpOp, Cond, EffectSource}; + use crate::exiftool_tables::cond::{CmpOp, Cond, Ctx, EffectSource}; use crate::exiftool_tables::ifd_schema::IfdVariantGroup; use crate::exiftool_tables::{ ExprId, GateA, IfdFlags, Omitted, PrintConv, SizeExpectation, TagGroups, U16SizeCheck, @@ -3367,6 +3402,100 @@ mod tests { assert_eq!(out, vec!["parent"]); } + #[test] + fn refused_owned_serial_walk_restores_child_state_but_keeps_parent_effects() { + let source = find_serial_table("Canon", "AFInfo2").unwrap(); + let mut entries = source.entries.to_vec(); + let mut later = entries[3].alternatives.to_vec(); + // Deliberately inconsistent artifact: the earlier NumAFPoints + // SetMember executes before this later unsupported source hook. + later[0].omitted.hook = true; + entries[3].alternatives = Box::leak(later.into_boxed_slice()); + let refused = Box::leak(Box::new(SerialTable { + entries: Box::leak(entries.into_boxed_slice()), + ..*source + })); + let data: Vec = [16u16, 2, 9, 1, 2, 3, 4, 5] + .into_iter() + .flat_map(u16::to_le_bytes) + .collect(); + let mut members = HashMap::from([ + ("AFInfo3", MemberValue::Num(1)), + ("NumAFPoints", MemberValue::Num(7)), + ]); + let before = members.clone(); + let mut ctx = Ctx::new(&mut members); + let mut out = Vec::new(); + let outcome = owned_serial_attempt(&mut ctx, &mut out, |ctx, out| { + let mut child = Vec::new(); + let result = process_serial_directory( + refused, + SerialDir { + data: &data, + dir_start: 0, + dir_len: data.len(), + base: 0, + data_pos: 0, + byte_order: ByteOrder::Little, + }, + ctx, + &mut IfdSerialSink { out: &mut child }, + ); + assert!(result.tainted); + assert_eq!(ctx.members.get("NumAFPoints"), Some(&MemberValue::Num(9))); + assert_eq!(child.len(), 2, "a real generated prefix was read"); + DescendOutcome::Serial(finish_serial_child(out, child, &result)) + }); + assert_eq!(outcome, DescendOutcome::Serial(SerialSubdirRead::Refused)); + assert!(out.is_empty()); + assert_eq!(*ctx.members, before); + assert!( + Cond::MemberCmp { + member: "NumAFPoints", + op: CmpOp::Eq, + value: 7, + } + .eval(&mut ctx) + ); + assert!( + Cond::MemberTruthy { + member: "AFInfo3", + negate: false, + } + .eval(&mut ctx) + ); + } + + #[test] + fn missing_proof_on_owned_serial_edge_is_refusal_not_legacy_fallback() { + let mut tags = SERIAL_EDGE_TAGS.to_vec(); + tags[0].subdir.as_mut().unwrap().validation = None; + let parent = Box::leak(Box::new(IfdTable { + tags: Box::leak(tags.into_boxed_slice()), + ..SERIAL_EDGE + })); + let data = serial_child_parent(ByteOrder::Little, 16); + let mut members = HashMap::from([("AFInfo3", MemberValue::Num(1))]); + let mut ctx = Ctx::new(&mut members); + let mut out = Vec::new(); + let reads = process_exif_decoded( + parent, + IfdDir { + data: &data, + ifd_start: 0, + base: Some(0), + byte_order: ByteOrder::Little, + group1: None, + }, + &mut ctx, + &mut out, + ) + .unwrap(); + assert_eq!(reads.serial_subdirs, vec![(0, SerialSubdirRead::Refused)]); + assert!(out.is_empty()); + assert_eq!(ctx.members.get("AFInfo3"), Some(&MemberValue::Num(1))); + } + // FujiFilm.pm:709-714 and Olympus.pm:809-822 supply direct (not // `_variants`) member conditions. `process_exif` must use the caller's // file-level state for both string predicate forms; a missing or wrong diff --git a/src/exiftool_tables/ifd_tables.rs b/src/exiftool_tables/ifd_tables.rs index 152179eb3..4cf31d028 100644 --- a/src/exiftool_tables/ifd_tables.rs +++ b/src/exiftool_tables/ifd_tables.rs @@ -2201,8 +2201,8 @@ pub static IFD_CANON_MAIN: IfdTable = IfdTable { dir_name: None, validate: true, validation: None, - processor: IfdSubdirProcessor::Serial, - unwalked: None, + processor: IfdSubdirProcessor::Native, + unwalked: Some("serial Validate lacks authenticated primitive"), }), }, IfdTag { diff --git a/src/parsers/tiff/makernotes/canon.rs b/src/parsers/tiff/makernotes/canon.rs index 259243336..aef55ea07 100644 --- a/src/parsers/tiff/makernotes/canon.rs +++ b/src/parsers/tiff/makernotes/canon.rs @@ -791,13 +791,8 @@ const CANON_SERIAL_NUMBER: u16 = 0x000C; const CANON_CAMERA_INFO: u16 = 0x000D; const CANON_CUSTOM_FUNCTIONS: u16 = 0x000F; const CANON_AF_INFO: u16 = 0x0012; -const CANON_AF_INFO2: u16 = 0x0026; /// Canon.pm:1726 -- 16-byte undef value rendered with `unpack("H*", $val)`. const CANON_IMAGE_UNIQUE_ID: u16 = 0x0028; -/// ExifTool Canon.pm:1764 -- `0x3c => { Name => 'AFInfo3', ... TagTable => -/// 'Image::ExifTool::Canon::AFInfo2' }`. A second MakerNote tag carrying the very same -/// `%Canon::AFInfo2` record, used by the G1XmkII and the EOS M bodies after it. -const CANON_AF_INFO3: u16 = 0x003C; /// ExifTool Canon.pm:1757 -- `0x38 => { Name => 'BatteryType', Writable => /// 'undef', Condition => '$count == 76', RawConv => /// '$val=~/^.{4}([^\0]+)/s ? $1 : undef' }`: the first four bytes are a @@ -1141,36 +1136,6 @@ const AF_INFO_COUNT_WITH_UNKNOWN_TAIL: u32 = 36; /// `Format => 'int16u[8]'` on `Canon_AFInfo_0x000b` (Canon.pm:6492). const AF_INFO_UNKNOWN_TAIL_WORDS: usize = 8; -// AFInfo2 sequence indices (tag 0x0026) -// -// ExifTool `%Image::ExifTool::Canon::AFInfo2` (Canon.pm:6503), also serial -// (`PROCESS_PROC => \&ProcessSerialData`, `FORMAT => 'int16u'`). Keys 0..7 are scalars: -// -// ```text -// 0 => { Name => 'AFInfoSize', Unknown => 1, ... }, -// 1 => { Name => 'AFAreaMode', PrintConv => { ... } }, -// 2 => { Name => 'NumAFPoints', RawConv => '$$self{NumAFPoints} = $val', }, -// 3 => { Name => 'ValidAFPoints', ... }, -// 4 => { Name => 'CanonImageWidth', ... }, -// 5 => { Name => 'CanonImageHeight', ... }, -// 6 => { Name => 'AFImageWidth', ... }, -// 7 => 'AFImageHeight', -// 8 => { Name => 'AFAreaWidths', Format => 'int16s[$val{2}]', }, -// 9 => { Name => 'AFAreaHeights', Format => 'int16s[$val{2}]', }, -// 10 => { Name => 'AFAreaXPositions', Format => 'int16s[$val{2}]', }, -// 11 => { Name => 'AFAreaYPositions', Format => 'int16s[$val{2}]', }, -// 12 => { Name => 'AFPointsInFocus', Format => 'int16s[int(($val{2}+15)/16)]', ... }, -// ``` -const AF_INFO2_AF_AREA_MODE: usize = 1; -const AF_INFO2_NUM_AF_POINTS: usize = 2; -const AF_INFO2_VALID_AF_POINTS: usize = 3; -const AF_INFO2_CANON_IMAGE_WIDTH: usize = 4; -const AF_INFO2_CANON_IMAGE_HEIGHT: usize = 5; -const AF_INFO2_AF_IMAGE_WIDTH: usize = 6; -const AF_INFO2_AF_IMAGE_HEIGHT: usize = 7; -/// First variable-length slot of `%Canon::AFInfo2` (Perl key 8, `AFAreaWidths`). -const AF_INFO2_VARIABLE_START: usize = 8; - // FlashInfo array indices (tag 0x0003) const FLASH_INFO_FLASH_GUIDE_NUMBER: usize = 0; const FLASH_INFO_FLASH_THRESHOLD: usize = 1; @@ -2975,65 +2940,6 @@ const_decoder!( [(0, "Off"), (1, "AEB"), (2, "FEB"), (3, "ISO"), (4, "WB"),] ); -// Canon AFAreaMode decoder (AFInfo2 key 1) -// -// ExifTool `%Image::ExifTool::Canon::AFInfo2` key 1 (Canon.pm:6517): -// -// ```text -// 1 => { -// Name => 'AFAreaMode', -// PrintConv => { -// 0 => 'Off (Manual Focus)', -// 1 => 'AF Point Expansion (surround)', #PH -// 2 => 'Single-point AF', -// # 3 - n/a -// 4 => 'Auto', #forum6237 (AiAF on A570IS) -// 5 => 'Face Detect AF', -// 6 => 'Face + Tracking', #PH (NC, EOS M, live view) -// 7 => 'Zone AF', #46 -// 8 => 'AF Point Expansion (4 point)', #46/PH/forum6237 -// 9 => 'Spot AF', #46 -// 10 => 'AF Point Expansion (8 point)', #forum6237 -// 11 => 'Flexizone Multi (49 point)', #PH (NC, EOS M, live view; 750D 49 points) -// 12 => 'Flexizone Multi (9 point)', #PH (750D, 9 points) -// 13 => 'Flexizone Single', #PH (EOS M default, live view) ... -// 14 => 'Large Zone AF', #PH/forum6237 (7DmkII) -// 16 => 'Large Zone AF (vertical)', #forum16223 -// 17 => 'Large Zone AF (horizontal)', #forum16223 -// 19 => 'Flexible Zone AF 1', #github268 (R7) -// 20 => 'Flexible Zone AF 2', #github268 (R7) -// 21 => 'Flexible Zone AF 3', #github268 (R7) -// 22 => 'Whole Area AF', #github268 (R7) -// }, -// }, -// ``` -const_decoder!( - pub AF_AREA_MODE, - i16, - [ - (0, "Off (Manual Focus)"), - (1, "AF Point Expansion (surround)"), - (2, "Single-point AF"), - (4, "Auto"), - (5, "Face Detect AF"), - (6, "Face + Tracking"), - (7, "Zone AF"), - (8, "AF Point Expansion (4 point)"), - (9, "Spot AF"), - (10, "AF Point Expansion (8 point)"), - (11, "Flexizone Multi (49 point)"), - (12, "Flexizone Multi (9 point)"), - (13, "Flexizone Single"), - (14, "Large Zone AF"), - (16, "Large Zone AF (vertical)"), - (17, "Large Zone AF (horizontal)"), - (19, "Flexible Zone AF 1"), - (20, "Flexible Zone AF 2"), - (21, "Flexible Zone AF 3"), - (22, "Whole Area AF"), - ] -); - // Canon white balance decoder for ShotInfo // More detailed than standard EXIF white balance const_decoder!( @@ -5330,13 +5236,17 @@ fn parse_canon_makernote_directory( hand_entries += 1; let entry_index = hand_entries - 1; // A source-selected serial child keeps its parent entry index from - // the IFD walk. Only a handled native result retires this arm: an - // unavailable/tainted route falls through to the established hand - // producer, while a false parent Condition or Validate deliberately - // suppresses it with native's no-output result. + // the IFD walk. An enabled route owns this entry on both native + // completion and explicit execution refusal. Its retired manual + // producer cannot reinterpret invalid or unsupported bytes. if let Some(engine) = main_engine.as_mut() - && engine.replay_serial(entry_index, &mut tags, &mut value_forms) - == Some(crate::exiftool_tables::SerialSubdirRead::Handled) + && matches!( + engine.replay_serial(entry_index, &mut tags, &mut value_forms), + Some( + crate::exiftool_tables::SerialSubdirRead::Handled + | crate::exiftool_tables::SerialSubdirRead::Refused + ) + ) { return; } @@ -7030,141 +6940,6 @@ fn parse_canon_makernote_directory( } } - // AFInfo2 (tag 0x0026) - autofocus information used by newer Canon models. - // AFInfo3 (tag 0x003c) is the same `%Canon::AFInfo2` record under a second - // tag id (Canon.pm:1764). The two are alternatives, not siblings: all 42 - // sample-corpus files that carry 0x003c carry no 0x0026 at all, so reading - // only 0x0026 left them with no AF tags whatsoever. - CANON_AF_INFO2 | CANON_AF_INFO3 => { - if let Some(array) = - extract_canon_i16_array_with_base(entry, ifd_data, byte_order, base) - { - if let Some(&mode) = array.get(AF_INFO2_AF_AREA_MODE) { - tags.insert("Canon:AFAreaMode".to_string(), AF_AREA_MODE.decode(mode)); - } - - // Same `ProcessSerialData` rule as AFInfo above: present slots are - // reported unconditionally. Keys 3/4/5 were transcribed into the - // comment on the index constants but never read, so ValidAFPoints, - // CanonImageWidth and CanonImageHeight went missing on every body - // that writes AFInfo2 (e.g. the 1D Mk III, where ExifTool prints - // `ValidAFPoints: 45`, `CanonImageWidth: 3888`, - // `CanonImageHeight: 2592`). - let num_points = array.get(AF_INFO2_NUM_AF_POINTS).copied().unwrap_or(0); - if let Some(&points) = array.get(AF_INFO2_NUM_AF_POINTS) { - tags.insert("Canon:NumAFPoints".to_string(), points.to_string()); - } - if let Some(&valid_points) = array.get(AF_INFO2_VALID_AF_POINTS) { - tags.insert("Canon:ValidAFPoints".to_string(), valid_points.to_string()); - } - if let Some(&width) = array.get(AF_INFO2_CANON_IMAGE_WIDTH) { - tags.insert("Canon:CanonImageWidth".to_string(), width.to_string()); - } - if let Some(&height) = array.get(AF_INFO2_CANON_IMAGE_HEIGHT) { - tags.insert("Canon:CanonImageHeight".to_string(), height.to_string()); - } - - if let Some(&width) = array.get(AF_INFO2_AF_IMAGE_WIDTH) { - tags.insert("Canon:AFImageWidth".to_string(), width.to_string()); - } - if let Some(&height) = array.get(AF_INFO2_AF_IMAGE_HEIGHT) { - tags.insert("Canon:AFImageHeight".to_string(), height.to_string()); - } - - // Keys 8+ are variable-length: AFAreaWidths[n], AFAreaHeights[n], - // AFAreaXPositions[n], AFAreaYPositions[n], then AFPointsInFocus as - // ceil(n/16) 16-bit words. EOS bodies carry AFPointsSelected in the - // next same-sized bitset; other bodies use that slot for an unknown - // record with one additional word (Canon.pm AFInfo2 keys 13-14). - if num_points > 0 { - let n = num_points as usize; - let widths_start = AF_INFO2_VARIABLE_START; - let heights_start = widths_start + n; - let x_start = heights_start + n; - let y_start = x_start + n; - let focus_start = y_start + n; - let focus_words = n.div_ceil(16); - - if array.len() >= heights_start { - tags.insert( - "Canon:AFAreaWidths".to_string(), - join_i16_slice(&array[widths_start..heights_start]), - ); - } - if array.len() >= x_start { - tags.insert( - "Canon:AFAreaHeights".to_string(), - join_i16_slice(&array[heights_start..x_start]), - ); - } - if array.len() >= y_start { - tags.insert( - "Canon:AFAreaXPositions".to_string(), - join_i16_slice(&array[x_start..y_start]), - ); - } - if array.len() >= focus_start { - tags.insert( - "Canon:AFAreaYPositions".to_string(), - join_i16_slice(&array[y_start..focus_start]), - ); - } - if array.len() >= focus_start + focus_words { - tags.insert( - "Canon:AFPointsInFocus".to_string(), - decode_bits_16(&array[focus_start..focus_start + focus_words]), - ); - } - let selected_start = focus_start + focus_words; - if self_model.contains("EOS") { - if array.len() >= selected_start + focus_words { - tags.insert( - "Canon:AFPointsSelected".to_string(), - decode_bits_16( - &array[selected_start..selected_start + focus_words], - ), - ); - } - } else { - // Key 13's second branch (Canon.pm:6595-6599), - // `Canon_AFInfo2_0x000d`: no Condition, so a non-EOS body - // always lands here, and its - // `Format => 'int16s[int(($val{2}+15)/16)+1]'` is one word - // WIDER than the EOS branch's AFPointsSelected. It carries - // `Unknown => 1` so ExifTool prints nothing for it, but - // ProcessSerialData still advances the cursor past it - // (Canon.pm:10585 gates only the FoundTag, not the `$pos += - // $len` at Canon.pm:10592). Missing that extra word is why - // key 14 was never reached. - // - // Key 14 (Canon.pm:6600-6604) is then `PrimaryAFPoint`, - // Condition `$$self{Model} !~ /EOS/ and not $$self{AFInfo3}` - // -- one `int16u` under the table's `FORMAT => 'int16u'` - // (Canon.pm:6507), with no PrintConv, so the raw word is the - // printed value. A PowerShot SX740 HS writes 65535 there, - // which is why it is read unsigned rather than through the - // i16 the rest of this record uses. - // - // `$$self{AFInfo3}` is a per-file sticky flag set by tag - // 0x3c's own Condition (Canon.pm:1766), not a per-record one. - // Testing this entry's tag id instead is equivalent here - // because 0x26 sorts before 0x3c in the IFD, so a file - // carrying both would still see the flag clear while 0x26 is - // processed -- and no corpus file carries both anyway. - let primary = selected_start + focus_words + 1; - if entry.tag_id != CANON_AF_INFO3 - && let Some(&value) = array.get(primary) - { - tags.insert( - "Canon:PrimaryAFPoint".to_string(), - af_info_slot_as_int16u(value), - ); - } - } - } - } - } - // CustomFunctions (tag 0x000F). // // ExifTool Canon.pm:1500 picks a per-body table (`%CanonCustom::FunctionsXXX` diff --git a/src/parsers/tiff/makernotes/canon/main_engine.rs b/src/parsers/tiff/makernotes/canon/main_engine.rs index 2d2e1218f..d41705bb2 100644 --- a/src/parsers/tiff/makernotes/canon/main_engine.rs +++ b/src/parsers/tiff/makernotes/canon/main_engine.rs @@ -71,8 +71,9 @@ //! CameraInfo*, ColorData). [`is_canon_main_row`] drops every ordinary child //! row. A source-authenticated serial edge is the narrow exception: the IFD //! engine returns its rows with the parent entry index, and this buffer replays -//! them there while preserving a hand fallback for unavailable or tainted -//! execution. Other newly enabled targets still turn the fence test red. +//! them there. Enabled serial routes have sole ownership: unsupported child +//! execution is an explicit refusal, never a return to a retired manual +//! producer. Other newly enabled targets still turn the fence test red. use std::collections::HashMap; @@ -208,9 +209,9 @@ impl MainEngineRows { } /// Replay every child row generated for this parent entry. `Handled` - /// consumes the parent hand arm even if native produced no rows; `Fallback` - /// leaves that arm available because the shared route was not authenticated - /// or became tainted. + /// consumes the parent entry even if native produced no rows. `Refused` + /// also owns the entry but publishes no speculative output. `Fallback` + /// is reserved for a table whose ownership has not been migrated. pub(super) fn replay_serial( &mut self, entry: usize, diff --git a/tools/exiftool-tables/codegen.py b/tools/exiftool-tables/codegen.py index ee6f01da6..aff995d86 100644 --- a/tools/exiftool-tables/codegen.py +++ b/tools/exiftool-tables/codegen.py @@ -2395,6 +2395,7 @@ def compile_ifd_subdir(tag, stats, ctx, enclosing=None): validate = sd.get("Validate") is not None validation_src = "None" + serial_validation_refused = False if validate: # The existing IFD/binary paths deliberately retain their historical # refusal. Only a source-selected serial target may opt into the @@ -2406,6 +2407,9 @@ def compile_ifd_subdir(tag, stats, ctx, enclosing=None): # ($val, $dirData, $subdirStart, $size)` (Exif.pm:7082) is Perl # over directory bytes. stats["ifd_subdir_refused_validate"] += 1 + if target_kind == "serial": + serial_validation_refused = True + unwalked.append("serial Validate lacks authenticated primitive") else: validation_src = compiled.rust(rust_str) stats["ifd_subdir_validate_compiled"] += 1 @@ -2440,7 +2444,7 @@ def compile_ifd_subdir(tag, stats, ctx, enclosing=None): f"fix_format: {fix_src}, sub_ifd: {'true' if sub_ifd else 'false'}, " f"max_subdirs: {max_src}, dir_name: {dir_src}, " f"validate: {'true' if validate else 'false'}, validation: {validation_src}, " - f"processor: IfdSubdirProcessor::{'Serial' if target_kind == 'serial' else 'Native'}, " + f"processor: IfdSubdirProcessor::{'Serial' if target_kind == 'serial' and not serial_validation_refused else 'Native'}, " f"unwalked: {unwalked_src} }})" ) diff --git a/tools/exiftool-tables/test_codegen_ifd.py b/tools/exiftool-tables/test_codegen_ifd.py index 2d98763b4..8b942473c 100644 --- a/tools/exiftool-tables/test_codegen_ifd.py +++ b/tools/exiftool-tables/test_codegen_ifd.py @@ -535,8 +535,9 @@ def emit(subdir): broken["__deparse"] = broken["__deparse"].replace("Get16u", "Get32u") ctx.validation_helpers = {NAME: broken} src, stats = emit({"TagTable": "Image::ExifTool::Any::Child", "Validate": CALL}) - self.assertIn("processor: IfdSubdirProcessor::Serial", src) + self.assertIn("processor: IfdSubdirProcessor::Native", src) self.assertIn("validation: None", src) + self.assertIn('unwalked: Some("serial Validate lacks authenticated primitive")', src) self.assertEqual(stats["ifd_subdir_refused_validate"], 1) def test_base_through_the_existing_grammar(self): From 62535db76595c2083f5d0af13f70b7e219efe02f Mon Sep 17 00:00:00 2001 From: swackhamer Date: Sun, 13 Sep 2026 10:09:36 -0500 Subject: [PATCH 08/12] fix(tables): authenticate serial IFD processor edges --- tools/exiftool-tables/oracle.pl | 63 ++++- .../exiftool-tables/test_oracle_processors.py | 47 ++++ tools/exiftool-tables/test_verify_ifd.py | 248 ++++++++++++++++++ tools/exiftool-tables/verify.py | 199 +++++++++++--- 4 files changed, 523 insertions(+), 34 deletions(-) diff --git a/tools/exiftool-tables/oracle.pl b/tools/exiftool-tables/oracle.pl index d404f82b1..718881e55 100755 --- a/tools/exiftool-tables/oracle.pl +++ b/tools/exiftool-tables/oracle.pl @@ -105,8 +105,10 @@ # Condition (6) # IFD MODULE TABLE KEY SUBDIR TAGTABLE START BASE PROCESSPROC BYTEORDER VALIDATE # FIXFORMAT SUBIFD MAXSUBDIRS DIRNAME (15) -# IFD MODULE TABLE KEY VALIDATION EXPRESSION CALLEE SOURCE_FILE SOURCE_SHA256 (9) -# -- scalar SubDirectory Validate provenance +# IFD MODULE TABLE KEY VALIDATION EXPRESSION CALLEE SOURCE_FILE SOURCE_SHA256 JSON-HELPER (10) +# -- scalar SubDirectory Validate provenance/body fact +# IFD MODULE TABLE KEY PROCESSOR TARGET_MODULE TARGET_TABLE ORIGIN JSON-FACT (9) +# -- final effective child processor after all modules load # # KEY is the integer tag id as ExifTool keys it, or `"$k#$i"` for the i-th # alternative of a `_variants` arrayref (same convention as the binary rows). @@ -154,6 +156,17 @@ sub txt { # Source-file provenance comes from the live callee CODE ref, not the dump # or generated audit record. A changed helper invalidates a stale artifact # even when its owning module's tag tables did not change. +sub keyed_validation_helper_fact { + my ($expression) = @_; + return processor_unresolved_fact('', 'validation_expression_unavailable') + unless defined $expression && !ref $expression + && $expression =~ /^\s*((?:[A-Za-z_]\w*::)+[A-Za-z_]\w*)\s*\(/; + my $callee = $1; + no strict 'refs'; + my $cv = *{$callee}{CODE}; + return processor_code_ref_fact($cv, $callee); +} + sub keyed_validation_source { my ($expression) = @_; return ('', '', '') unless defined $expression && !ref $expression; @@ -673,6 +686,12 @@ sub dash_text { return ref $v ? '__REF__' : clean($v); } +# Resolve effective SubDirectory processors only after all modules have loaded: +# an override or target table can be rebound by a later module. The raw +# `SUBDIR` record remains the source spelling; this separate fact is the +# authenticated executable binding the generated serial edge depends on. +my @ifd_subdir_processors; + # Emit every IFD row for one tag-info entry `$e` at `$key`. `$plain` is true # for a scalar-keyed entry and false for a `_variants` alternative (only the # former gets a CONDITION row -- see the header). @@ -747,9 +766,14 @@ sub emit_ifd_entry { || (defined $fix && !ref $fix && $fix eq 'ifd')) ? 1 : 0; print join("\t", @p, 'SUBDIR', $tagtable, $start, $base, $proc, $bo, $validate, dash_text($fix), $subifd, $max, $dir), "\n"; + push @ifd_subdir_processors, { + module => $mod, table => $sym, key => $key, tagtable => $tagtable, + override => (ref $sd eq 'HASH' ? $sd->{ProcessProc} : undef), + }; if (ref $sd eq 'HASH' && defined $sd->{Validate} && !ref $sd->{Validate}) { print join("\t", @p, 'VALIDATION', clean($sd->{Validate}), - keyed_validation_source($sd->{Validate})), "\n"; + keyed_validation_source($sd->{Validate}), + JSON::PP->new->canonical->encode(keyed_validation_helper_fact($sd->{Validate}))), "\n"; } } @@ -926,6 +950,39 @@ sub emit_ifd_table { print join("\t", 'NATIVE_PROCESSOR', $mod, $sym, JSON::PP->new->canonical->encode($fact)), "\n"; } + +# The raw IFD SUBDIR fact says whether ProcessProc was present, but its target +# table and any CODE override are only final after the complete module walk. +# Bind that effective processor here with the same source/deparse provenance +# used for NATIVE_PROCESSOR. This is source-derived protocol, never a +# Canon/table selector. +for my $edge (sort { + $a->{module} cmp $b->{module} + || $a->{table} cmp $b->{table} + || $a->{key} cmp $b->{key} +} @ifd_subdir_processors) { + my ($target_mod, $target_table) = ('-', '-'); + if ($edge->{tagtable} =~ /^Image::ExifTool::([A-Za-z_]\w*)::([A-Za-z_]\w*)$/) { + ($target_mod, $target_table) = ($1, $2); + } + my ($origin, $fact); + if (defined $edge->{override}) { + $origin = 'override'; + $fact = ref($edge->{override}) eq 'CODE' + ? processor_code_ref_fact($edge->{override}, 'SubDirectory::ProcessProc') + : processor_unresolved_fact('SubDirectory::ProcessProc', 'subdirectory_processproc_not_code'); + } elsif ($target_mod ne '-') { + $origin = 'target'; + $fact = $processor_facts{"$target_mod\t$target_table"} + // processor_unresolved_fact("Image::ExifTool::${target_mod}::${target_table}::PROCESS_PROC", + 'target_processproc_unavailable'); + } else { + $origin = 'unresolved'; + $fact = processor_unresolved_fact('SubDirectory::TagTable', 'target_table_unparseable'); + } + print join("\t", 'IFD', $edge->{module}, $edge->{table}, $edge->{key}, 'PROCESSOR', + $target_mod, $target_table, $origin, JSON::PP->new->canonical->encode($fact)), "\n"; +} for my $key (sort keys %processor_tables) { my ($mod, $sym) = split /\t/, $key, 2; my $table = $processor_tables{$key}{table}; diff --git a/tools/exiftool-tables/test_oracle_processors.py b/tools/exiftool-tables/test_oracle_processors.py index 7bfcfcfbe..941ee632f 100644 --- a/tools/exiftool-tables/test_oracle_processors.py +++ b/tools/exiftool-tables/test_oracle_processors.py @@ -65,6 +65,16 @@ def write_fixture(self): { Format => 'int8u', Count => undef }, ], ); + # Holder is an IFD-style table. Its child selection has no + # ProcessProc override, so oracle.pl resolves the target table's + # PROCESS_PROC only after the full module walk. + our %Holder = ( + GROUPS => { 0 => 'EXIF', 1 => 'Fixture', 2 => 'Image' }, + 1 => { Name => 'Child', SubDirectory => { TagTable => 'Image::ExifTool::Fixture::Words' } }, + ); + our %Words = ( PROCESS_PROC => \\&ProcessWords, GROUPS => { 0 => 'EXIF' }, + 1 => { Name => 'Word' }, + ); our %Empty = ( PROCESS_PROC => \\&ProcessWords, GROUPS => { 0 => 'EXIF' } ); 1; """), encoding="utf-8") @@ -128,6 +138,43 @@ def test_table_and_row_streams_preserve_variants_properties_and_zero_rows(self): self.assertTrue(unnamed["properties"]["Count"]["present"]) self.assertEqual(unnamed["properties"]["Count"]["value"], {"kind": "undef"}) + def test_ifd_processor_stream_joins_default_target_after_module_load(self): + result = subprocess.run( + ["/usr/bin/perl", str(ORACLE), str(self.lib)], + check=True, text=True, capture_output=True, + ) + rows = [line.split("\t") for line in result.stdout.splitlines()] + record = next(row for row in rows if row[:5] == ["IFD", "Fixture", "Holder", "1", "PROCESSOR"]) + self.assertEqual(record[5:8], ["Fixture", "Words", "target"]) + fact = json.loads(record[8]) + self.assertTrue(fact["resolved"]) + self.assertEqual(fact["__name"], "Image::ExifTool::Fixture::ProcessWords") + self.assertEqual(fact["source_file"], "Image/ExifTool/Fixture.pm") + # `NATIVE_PROCESSOR` is the target-side independently emitted record; + # identical provenance makes stale target-body artifacts detectable. + processors, _tables, _rows = self.facts() + self.assertEqual(fact, processors[("Fixture", "Words")]) + + def test_ifd_processor_stream_records_explicit_override(self): + text = self.fixture.read_text(encoding="utf-8") + text = text.replace( + " our %Holder = (", " sub AlternateProcessor { return 1; }\n our %Holder = (", 1 + ).replace( + "SubDirectory => { TagTable => 'Image::ExifTool::Fixture::Words' }", + "SubDirectory => { TagTable => 'Image::ExifTool::Fixture::Words', ProcessProc => \\&AlternateProcessor }", + 1, + ) + self.fixture.write_text(text, encoding="utf-8") + result = subprocess.run( + ["/usr/bin/perl", str(ORACLE), str(self.lib)], + check=True, text=True, capture_output=True, + ) + rows = [line.split("\t") for line in result.stdout.splitlines()] + record = next(row for row in rows if row[:5] == ["IFD", "Fixture", "Holder", "1", "PROCESSOR"]) + self.assertEqual(record[5:8], ["Fixture", "Words", "override"]) + self.assertEqual(json.loads(record[8])["__name"], + "Image::ExifTool::Fixture::AlternateProcessor") + def test_oracle_preserves_unresolved_bare_reader(self): self.later.write_text("package Image::ExifTool::ZZLater; 1;\n", encoding="utf-8") text = self.fixture.read_text(encoding="utf-8") diff --git a/tools/exiftool-tables/test_verify_ifd.py b/tools/exiftool-tables/test_verify_ifd.py index 6029ca14e..dbe060f89 100644 --- a/tools/exiftool-tables/test_verify_ifd.py +++ b/tools/exiftool-tables/test_verify_ifd.py @@ -12,13 +12,18 @@ tree, which must PASS. Run with `python3 -m unittest discover -s tools/exiftool-tables -p 'test*.py'`. """ +import copy +import hashlib +import os import pathlib import re +import types import tempfile import unittest import reachability import verify +import verify_serial_directory HERE = pathlib.Path(__file__).resolve().parent SAMPLE = HERE / "fixtures" / "ifd_tables_sample.rs" @@ -792,6 +797,249 @@ def test_grammar(self): self.assertIsNone(e(self._fact(maxsubdirs="many"))[0]) +class SerialProcessorProvenance(unittest.TestCase): + """The serial edge exception is a three-way native/artifact join. + + These use raw oracle-shaped facts and a tiny independently shaped artifact + object. They do not invoke codegen: mutating any provenance operand must + make a custom target unwalked again. + """ + + @staticmethod + def _fact(): + return { + "tagtable": "Image::ExifTool::Canon::AFInfo2", "start": "-", "base": "-", + "processproc": "-", "byteorder": "-", "validate": True, "fixformat": "-", + "subifd": False, "maxsubdirs": "-", "dirname": "-", + } + + @staticmethod + def _code(deparse="sub body"): + return { + "__perl": "CODE", "resolved": True, + "__name": "Image::ExifTool::Canon::ProcessSerialData", + "source_file": "Image/ExifTool/Canon.pm", + "source_sha256": "a" * 64, + "__deparse": deparse, + } + + def _helper(self, deparse=None): + return { + "__perl": "CODE", "resolved": True, + "__name": "Image::ExifTool::Canon::Validate", + "source_file": "Image/ExifTool/Canon.pm", "source_sha256": "a" * 64, + "__deparse": deparse or ( + "($$@) { package Image::ExifTool::Canon; use strict; " + "(my($dataPt, $offset, @vals) = @_); " + "(my($dataVal) = &Image::ExifTool::Get16u($dataPt, $offset)); " + "my($val); foreach $val (@vals) { (($val == $dataVal) and (return 1)); } " + "(return (undef)); }" + ), + } + + def _inputs(self): + fact = self._fact() + code = self._code() + identity = ( + code["__name"], code["source_file"], code["source_sha256"], + hashlib.sha256(code["__deparse"].encode("utf-8")).hexdigest(), + ) + tables = {("Canon", "AFInfo2"): types.SimpleNamespace(processor=identity)} + edge = ("Canon", "AFInfo2", "target", code) + return fact, edge, {("Canon", "AFInfo2"): code}, tables, self._helper() + + def test_authenticated_serial_target_is_walkable(self): + fact, edge, processors, tables, helper = self._inputs() + got, _ = verify.expected_ifd_edge( + fact, ("Canon", "Main"), edge, processors, tables, helper + ) + self.assertEqual((got["processor"], got["unwalked"]), ("Serial", False)) + + def test_processor_provenance_mutations_fail_closed(self): + fact, edge, processors, tables, helper = self._inputs() + # A stale serial artifact body/source record, a changed effective + # override, or a missing protocol record must not leave a Native edge + # executable merely because its TagTable looks familiar. + for changed_edge, changed_tables in ( + ((edge[0], edge[1], edge[2], self._code("changed body")), tables), + (edge, {("Canon", "AFInfo2"): types.SimpleNamespace(processor=( + "Image::ExifTool::Canon::ProcessSerialData", "Image/ExifTool/Canon.pm", + "b" * 64, tables[("Canon", "AFInfo2")].processor[3], + ))}), + (None, tables), + ): + got, _ = verify.expected_ifd_edge( + fact, ("Canon", "Main"), changed_edge, processors, changed_tables, helper + ) + self.assertEqual((got["processor"], got["unwalked"]), ("Native", True)) + + def test_pending_serial_validation_is_native_and_unwalked(self): + # A ProcessSerialData target whose native Validate has no accepted + # primitive is an explicit non-execution state. It must not be called + # Serial merely because the table processor has a familiar name. + gen = copy.deepcopy(_parsed()) + oracle = copy.deepcopy(_oracle()) + key = ("Exif", "Main", "33424") + code = self._code() + identity = verify._processor_identity(code) + oracle.subdirs[key]["validate"] = True + oracle.processors[key] = ("Kodak", "IFD", "target", code) + oracle.table_processors[("Kodak", "IFD")] = code + # Geometry helper is source-authenticated but outside the closed + # U16 membership primitive, so Native+unwalked is the only honest + # staged spelling. + oracle.validation_functions[key] = self._helper("($$$) { return 0; }") + gen.tags[key]["subdir"].update({ + "validate": True, "validation": None, "processor": "Native", + "unwalked": "serial Validate lacks authenticated primitive", + }) + serial = {("Kodak", "IFD"): types.SimpleNamespace(processor=identity)} + _, failed = verify.verify_ifd(gen, oracle, serial_tables=serial) + self.assertEqual(failed, 0) + gen.tags[key]["subdir"]["processor"] = "Serial" + gen.tags[key]["subdir"]["unwalked"] = None + _, failed = verify.verify_ifd(gen, oracle, serial_tables=serial) + self.assertGreater(failed, 0) + + def test_processor_protocol_rows_are_strict(self): + code = self._code() + text = "\n".join(( + "NATIVE_PROCESSOR\tCanon\tAFInfo2\t" + verify.json.dumps(code, sort_keys=True), + "IFD\tCanon\tMain\t38\tPROCESSOR\tCanon\tAFInfo2\ttarget\t" + + verify.json.dumps(code, sort_keys=True), + )) + oracle = verify.parse_ifd_oracle(text) + self.assertEqual(oracle.processors[("Canon", "Main", "38")][:3], + ("Canon", "AFInfo2", "target")) + with self.assertRaises(SystemExit): + verify.parse_ifd_oracle(text + "\n" + text.splitlines()[1]) + + + +@unittest.skipUnless( + os.environ.get("OXIDEX_IFD_SERIAL_MUTATION_TEST") == "1", + "set OXIDEX_IFD_SERIAL_MUTATION_TEST=1 with canonical artifact/oracle paths", +) +class GeneratedSerialEdgeMutations(unittest.TestCase): + """Live-artifact mutation fence for the IFD -> serial provenance join. + + This is deliberately opt-in: the paths are produced by the sanctioned + canonical dump/regen flow, not copied into a hand-written fixture. When + requested, a missing input is an error, never a skip. + """ + + def setUp(self): + required = { + name: os.environ.get(name) for name in ( + "OXIDEX_IFD_GENERATED", "OXIDEX_SERIAL_GENERATED", "OXIDEX_IFD_SERIAL_ORACLE", + ) + } + for name, raw in required.items(): + self.assertTrue(raw, f"{name} is required when mutation test is enabled") + self.assertTrue(pathlib.Path(raw).is_file(), f"{name} is not a readable file: {raw!r}") + self.ifd_path = pathlib.Path(required["OXIDEX_IFD_GENERATED"]) + self.serial_path = pathlib.Path(required["OXIDEX_SERIAL_GENERATED"]) + self.oracle = verify.parse_ifd_oracle( + pathlib.Path(required["OXIDEX_IFD_SERIAL_ORACLE"]).read_text(encoding="utf-8") + ) + + def _verify(self, ifd_path, serial_path): + gen = verify.parse_ifd_rust(ifd_path) + serial = verify_serial_directory.parse_artifact(serial_path).tables + return verify.verify_ifd(gen, self.oracle, serial_tables=serial) + + @staticmethod + def _canon_tag_span(src, key): + # Work inside the actual Canon Main tag literal. The test carries no + # layout/value knowledge: it merely chooses authenticated source rows + # whose generated processor/validation facts are under audit. + table_begin = src.index('pub static IFD_CANON_MAIN') + marker = f"id: 0x{key:04x}," + begin = src.index(marker, table_begin) + end = src.find(" IfdTag {", begin + len(marker)) + return begin, len(src) if end < 0 else end + + @classmethod + def _replace_for_canon_key(cls, src, key, old, new): + begin, end = cls._canon_tag_span(src, key) + part = src[begin:end] + if part.count(old) != 1: + raise AssertionError((key, old, part.count(old))) + return src[:begin] + part.replace(old, new) + src[end:] + + @classmethod + def _remove_canon_validation(cls, src, key): + begin, end = cls._canon_tag_span(src, key) + label = "validation:" + value_start = src.index(label, begin, end) + len(label) + value_end = verify._value_span(src, value_start) + return src[:value_start] + " None" + src[value_end:] + + @staticmethod + def _replace_afinfo2_serial_processor(src, old, new): + begin = src.index('pub static SERIAL_CANON_AFINFO2') + end = src.find('pub static ', begin + 1) + if end < 0: + end = len(src) + part = src[begin:end] + if part.count(old) != 1: + raise AssertionError((old, part.count(old))) + return src[:begin] + part.replace(old, new) + src[end:] + + def test_actual_generated_canon_serial_provenance_mutations_fail(self): + import tempfile + + original_ifd = self.ifd_path.read_text(encoding="utf-8") + original_serial = self.serial_path.read_text(encoding="utf-8") + _, baseline = self._verify(self.ifd_path, self.serial_path) + self.assertEqual(baseline, 0, "generated source must verify before mutations") + mutations = { + "serial_to_native": ( + self._replace_for_canon_key( + original_ifd, 0x0026, + "processor: IfdSubdirProcessor::Serial,", + "processor: IfdSubdirProcessor::Native,", + ), original_serial, + ), + "validation_removed": ( + self._remove_canon_validation(original_ifd, 0x003c), + original_serial, + ), + "validation_operand_drift": ( + self._replace_for_canon_key( + original_ifd, 0x0026, "offset: 0,", "offset: 1," + ), original_serial, + ), + "serial_processor_body_drift": ( + original_ifd, + self._replace_afinfo2_serial_processor( + original_serial, + 'source_body_sha256: "ab5cb31e06a991a02f5569f1c008303c0505d28caf7ae2ec044aeede9c9f999c",', + 'source_body_sha256: "' + "0" * 64 + '",', + ), + ), + "serial_downgrade_with_validation_removed": ( + self._replace_for_canon_key( + self._replace_for_canon_key( + self._remove_canon_validation(original_ifd, 0x0026), 0x0026, + "processor: IfdSubdirProcessor::Serial,", + "processor: IfdSubdirProcessor::Native,", + ), 0x0026, "unwalked: None,", 'unwalked: Some("forced downgrade"),', + ), + original_serial, + ), + } + with tempfile.TemporaryDirectory() as td: + td = pathlib.Path(td) + for name, (ifd, serial) in mutations.items(): + ifd_path, serial_path = td / f"{name}-ifd.rs", td / f"{name}-serial.rs" + ifd_path.write_text(ifd, encoding="utf-8") + serial_path.write_text(serial, encoding="utf-8") + _, failed = self._verify(ifd_path, serial_path) + self.assertGreater(failed, 0, name) + + + class ReachabilityCensus(unittest.TestCase): def test_ifd_tables_and_edges(self): tables = reachability.parse_tables(SAMPLE, "ifd") diff --git a/tools/exiftool-tables/verify.py b/tools/exiftool-tables/verify.py index 0f09285c5..20b81391a 100644 --- a/tools/exiftool-tables/verify.py +++ b/tools/exiftool-tables/verify.py @@ -36,6 +36,7 @@ """ import argparse +import hashlib import re import subprocess import sys @@ -56,6 +57,7 @@ import verify_native_reader import verify_word_directory import verify_processor_inventory +import verify_serial_directory import verify_word_rows import json @@ -2493,6 +2495,13 @@ class IfdOracle(NamedTuple): conditions: set subdirs: dict validations: dict + # Full native helper CODE facts for each scalar Validate expression. + validation_functions: dict + # Authenticated effective processor facts for IFD SubDirectory edges. + # `table_processors` is the target table PROCESS_PROC; `processors` is + # the source edge's actual override/default binding after all modules load. + table_processors: dict + processors: dict reader_contracts: dict # `PCEXPR` rows: keys whose ExifTool PrintConv is a scalar expression. pcexprs: set @@ -2504,7 +2513,7 @@ def parse_ifd_oracle(out): a SystemExit: the oracle and the verifier move in lockstep, and a row silently ignored is a fact silently unverified.""" o = IfdOracle({}, {}, {}, {}, {}, defaultdict(dict), defaultdict(dict), set(), {}, {}, - {}, {}, {}, set(), set(), {}, {}, {}, set()) + {}, {}, {}, set(), set(), {}, {}, {}, {}, {}, {}, set()) for line in out.splitlines(): p = line.split("\t") if len(p) == 3 and p[0] == "NATIVE_READER_CONTRACT": @@ -2515,6 +2524,15 @@ def parse_ifd_oracle(out): except json.JSONDecodeError as exc: raise SystemExit(f"invalid native reader contract {p[1]!r}: {exc}") from exc continue + if len(p) == 4 and p[0] == "NATIVE_PROCESSOR": + key = (p[1], p[2]) + if key in o.table_processors: + raise SystemExit(f"duplicate native processor facts for {key}") + try: + o.table_processors[key] = json.loads(p[3]) + except json.JSONDecodeError as exc: + raise SystemExit(f"invalid native processor facts for {key}: {exc}") from exc + continue if p[0] != "IFD": continue n, kind = len(p), p[4] if len(p) > 4 else "" @@ -2560,10 +2578,22 @@ def parse_ifd_oracle(out): "byteorder": p[9], "validate": p[10] == "1", "fixformat": p[11], "subifd": p[12] == "1", "maxsubdirs": p[13], "dirname": p[14], } - elif kind == "VALIDATION" and n == 9: + elif kind == "PROCESSOR" and n == 9: + if k in o.processors: + raise SystemExit(f"duplicate IFD processor facts for {k}") + try: + o.processors[k] = (p[5], p[6], p[7], json.loads(p[8])) + except json.JSONDecodeError as exc: + raise SystemExit(f"invalid IFD processor facts for {k}: {exc}") from exc + elif kind == "VALIDATION" and n in (9, 10): if k in o.validations: raise SystemExit(f"duplicate IFD validation facts for {k}") o.validations[k] = tuple(p[5:9]) + if n == 10: + try: + o.validation_functions[k] = json.loads(p[9]) + except json.JSONDecodeError as exc: + raise SystemExit(f"invalid IFD validation helper facts for {k}: {exc}") from exc else: raise SystemExit( f"unrecognised IFD oracle row {line!r} -- oracle.pl and verify.py " @@ -2646,29 +2676,103 @@ def expected_ifd_format(spelling, count_decl): } -def expected_ifd_edge(fact, enclosing=None): - """Independently decide, from one oracle SUBDIR row, what edge the - generator may emit -> `(edge_facts, spec_refuses)`. `enclosing` is the - `(module, table)` the row belongs to. - - `edge_facts` is None when the facts put the edge outside the spec's - grammar for a reason that makes any emitted edge WRONG (a TagTable - present but unparseable; a Start outside `$valuePtr`/`$val` +- n; a - Base outside the arithmetic grammar; a FixFormat the schema cannot - spell; a MaxSubdirs that is not a count). Two shapes are expected - EMITTED BUT UNWALKED (`edge_facts["unwalked"]` True; spec v1.1, slice - IFD1): no TagTable at all -- ExifTool walks the pointer with the - enclosing table (Exif.pm:6939-6944), so the edge must name `enclosing` - -- and a ProcessProc other than ProcessBinaryData; the generated edge - must carry `unwalked: Some(..)` for exactly these and `None` otherwise. - `spec_refuses` is True when the only thing outside the spec is the - ByteOrder spelling: the spec lists six spellings, ExifTool itself - (Exif.pm:6974-6990) reads `/^Little/i`, `/^Big/i` and treats everything - else as detect-from-entry-count, so an edge emitted for, say, - `'Little-endian'` with `IfdByteOrder::Little` is not a wrong fact -- - `verify_ifd` accepts either that or a refusal for it, and counts the - former as a note.""" +def _processor_identity(fact): + """Return an independently checkable native CODE identity, or ``None``. + + This deliberately validates raw oracle facts rather than importing the + code generator's acceptance grammar. A serial edge has execution + authority only when both its effective binding and its target table's + PROCESS_PROC authenticate the same live CODE ref that the independently + parsed serial artifact records. + """ + if not isinstance(fact, dict): + return None + if fact.get("__perl") != "CODE" or fact.get("resolved") is not True: + return None + name, source_file, source_sha, deparse = ( + fact.get("__name"), fact.get("source_file"), + fact.get("source_sha256"), fact.get("__deparse"), + ) + if not all(isinstance(v, str) and v for v in (name, source_file, source_sha, deparse)): + return None + if (Path(source_file).is_absolute() or "\\" in source_file + or any(part in {"", ".", ".."} for part in source_file.split("/")) + or re.fullmatch(r"[0-9a-f]{64}", source_sha) is None): + return None + return (name, source_file, source_sha, hashlib.sha256(deparse.encode("utf-8")).hexdigest()) + + +_U16_MEMBERSHIP_HELPER = re.compile( + r"^\(\$\$@\)\{package(?:[A-Za-z_]\w*::)+[A-Za-z_]\w*;usestrict;" + r"\(my\(\$(?P[A-Za-z_]\w*),\$(?P[A-Za-z_]\w*),@(?P[A-Za-z_]\w*)\)=@_\);" + r"\(my\(\$(?P[A-Za-z_]\w*)\)=&Image::ExifTool::Get16u\(\$(?P=data),\$(?P=offset)\)\);" + r"my\(\$(?P[A-Za-z_]\w*)\);foreach\$(?P=item)\(@(?P=choices)\)\{" + r"\(\(\$(?P=item)==\$(?P=read)\)and\(return1\)\);\}\(return\(undef\)\);\}$" +) + + +def _u16_membership_helper(fact): + """Independently recognize the closed, side-effect-free U16 helper. + + This deliberately parses raw B::Deparse facts received from oracle.pl; + it does not import directory_validation/codegen. Whitespace is irrelevant + but every binding, read, comparison, return and absence of extra statement + is required. + """ + identity = _processor_identity(fact) + if identity is None: + return False + source = re.sub(r"\s+", "", fact["__deparse"]) + match = _U16_MEMBERSHIP_HELPER.fullmatch(source) + if match is None: + return False + values = match.groupdict() + return len({values["data"], values["offset"], values["read"], values["item"]}) == 4 + + +def _serial_processor_admitted(fact, edge_processor, table_processors, serial_tables, validation_helper): + """Whether this source edge may name an executable shared serial reader. + + The source ``SUBDIR`` spelling alone is insufficient: a generated serial + route must bind the exact native effective PROCESS_PROC (including a + ProcessProc override) to the target table's native PROCESS_PROC and to + the generated SerialProcessorFacts. All three joins are independent of + module/table names and fail closed on malformed or absent facts. + """ + if edge_processor is None or serial_tables is None or not _u16_membership_helper(validation_helper): + return False + target_mod, target_table, origin, effective = edge_processor + if origin not in {"target", "override"}: + return False + tagtable = fact.get("tagtable", "-") + match = _TAGTABLE_RE.match(tagtable) + if match is None or (target_mod, target_table) != match.groups(): + return False + native = _processor_identity(effective) + target = _processor_identity(table_processors.get((target_mod, target_table))) + table = serial_tables.get((target_mod, target_table)) + if native is None or target is None or table is None: + return False + # An override is only executable when it is the same processor that the + # target table itself declares. This blocks a stale/rebound CODE ref. + if native != target or native != table.processor: + return False + return native[0].endswith("::ProcessSerialData") + + +def expected_ifd_edge(fact, enclosing=None, edge_processor=None, table_processors=None, + serial_tables=None, validation_helper=None): + """Independently decide the permitted generated IFD edge facts. + + A non-binary ProcessProc is normally unwalked. The narrow exception is a + fully authenticated target ProcessSerialData route, independently joined + from ``IFD PROCESSOR`` and ``NATIVE_PROCESSOR`` oracle records to the + independently parsed serial artifact. This is a provenance check, not a + generator-recognizer import. + """ + table_processors = table_processors or {} unwalked = False + processor = "Native" if fact["tagtable"] == "-": if enclosing is None: return None, False @@ -2680,8 +2784,24 @@ def expected_ifd_edge(fact, enclosing=None): return None, False module, table = m.group(1), m.group(2) proc = fact["processproc"] - if proc != "-" and not proc.endswith("::ProcessBinaryData"): - unwalked = True + serial = _serial_processor_admitted( + fact, edge_processor, table_processors, serial_tables, validation_helper + ) + if serial: + processor = "Serial" + else: + # ProcessProc on the SubDirectory is an override. This slice changes + # the established generic IFD treatment only for a source-proven + # ProcessSerialData target: such a route becomes walkable solely after + # the full three-way join above. Other custom processors retain their + # existing Native/unwalked policy until they gain their own reader + # contract; broadening this verifier would reclassify unrelated IFD + # routes without an implementation or native execution proof. + target_identity = _processor_identity(table_processors.get((module, table))) + serial_target = (target_identity is not None + and target_identity[0].endswith("::ProcessSerialData")) + if ((proc != "-" and not proc.endswith("::ProcessBinaryData")) or serial_target): + unwalked = True start = fact["start"] if start == "-": start_v = ("ValuePtr", 0) @@ -2732,6 +2852,7 @@ def expected_ifd_edge(fact, enclosing=None): "max_subdirs": max_v, "dir_name": None if fact["dirname"] == "-" else fact["dirname"], "validate": fact["validate"], + "processor": processor, "unwalked": unwalked, }, spec_refuses @@ -2758,7 +2879,7 @@ def total(self): return self.ok + self.bad -def verify_ifd(gen, orc, show=10): +def verify_ifd(gen, orc, show=10, serial_tables=None): """Compare a `ParsedIfd` with an `IfdOracle` -> `(report_lines, failed)`. `failed` is the number of discrepancies (0 = PASS for this stage).""" T = lambda: _Tally(show) # noqa: E731 @@ -2957,13 +3078,16 @@ def verify_ifd(gen, orc, show=10): elif fact is None: t_edge.miss((k, "edge emitted but ExifTool has no SubDirectory")) else: - want, spec_refuses = expected_ifd_edge(fact, (k[0], k[1])) + want, spec_refuses = expected_ifd_edge( + fact, (k[0], k[1]), orc.processors.get(k), orc.table_processors, serial_tables, + orc.validation_functions.get(k) + ) if want is None: t_edge.miss((k, "edge emitted where the facts require a refusal", fact)) else: diffs = [ f for f in ("module", "table", "start", "byte_order", "fix_format", - "sub_ifd", "max_subdirs", "dir_name", "validate") + "sub_ifd", "max_subdirs", "dir_name", "validate", "processor") if edge[f] != want[f] ] if (edge["base"] != "None") != want["base_present"]: @@ -3002,7 +3126,10 @@ def verify_ifd(gen, orc, show=10): if spec_refuses: note_byteorder_outside_spec += 1 elif fact is not None and om["subdirectory"]: - want, spec_refuses = expected_ifd_edge(fact, (k[0], k[1])) + want, spec_refuses = expected_ifd_edge( + fact, (k[0], k[1]), orc.processors.get(k), orc.table_processors, serial_tables, + orc.validation_functions.get(k) + ) if want is not None and not spec_refuses: note_edge_refused_modelable += 1 @@ -3719,7 +3846,17 @@ def main(): if not gen_ifd.tables: sys.exit(f"parsed 0 IfdTable statics from {ifd_path} -- verifier is broken, " "not the generator; fix the parser before trusting a PASS") - ifd_lines, ifd_failed = verify_ifd(gen_ifd, parse_ifd_oracle(oracle_out), args.show) + ifd_oracle = parse_ifd_oracle(oracle_out) + serial_path = ifd_path.with_name("serial_tables.rs") + serial_tables = None + if "IfdSubdirProcessor" in ifd_path.read_text(encoding="utf-8"): + if not serial_path.is_file(): + raise SystemExit( + f"serial IFD schema requires {serial_path}, but it is absent; " + "cannot authenticate generated serial child edges" + ) + serial_tables = verify_serial_directory.parse_artifact(serial_path).tables + ifd_lines, ifd_failed = verify_ifd(gen_ifd, ifd_oracle, args.show, serial_tables) print("\n".join(ifd_lines)) else: print(f"\nIFD tables (slice I-1): SKIPPED -- {ifd_path} does not exist on this tree " From ff1e364c6ade47f471c7cd32a6150809de0d2acd Mon Sep 17 00:00:00 2001 From: swackhamer Date: Sun, 13 Sep 2026 10:19:01 -0500 Subject: [PATCH 09/12] Require live serial edge mutation checks and record full parity evidence --- .github/workflows/ci.yml | 6 +++++ docs/reference/afinfo2-production-plan.md | 31 +++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e2156f523..01106d147 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -582,6 +582,12 @@ jobs: export OXIDEX_TABLES_JSON="$DUMP" OXIDEX_SONY_DUMP="$DUMP" export OXIDEX_SERIAL_RUST="$GITHUB_WORKSPACE/src/exiftool_tables/serial_tables.rs" python3 tools/exiftool-tables/verify_serial_directory.py "$OXIDEX_SERIAL_RUST" "$DUMP" + # Exercise mutations of the actual generated parent/child artifacts, + # including a downgrade that removes both processor and validation. + export OXIDEX_IFD_SERIAL_MUTATION_TEST=1 + export OXIDEX_IFD_GENERATED="$GITHUB_WORKSPACE/src/exiftool_tables/ifd_tables.rs" + export OXIDEX_SERIAL_GENERATED="$OXIDEX_SERIAL_RUST" + export OXIDEX_IFD_SERIAL_ORACLE="$PROCESSORS" export OXIDEX_WORD_KEYED_RUST="$GITHUB_WORKSPACE/src/exiftool_tables/keyed_tables.rs" export OXIDEX_PROCESSOR_INVENTORY="$PROCESSORS" export OXIDEX_WORD_PROCESSOR=Image::ExifTool::CanonCustom::ProcessCanonCustom diff --git a/docs/reference/afinfo2-production-plan.md b/docs/reference/afinfo2-production-plan.md index 21f4f79ed..c25962b0e 100644 --- a/docs/reference/afinfo2-production-plan.md +++ b/docs/reference/afinfo2-production-plan.md @@ -98,3 +98,34 @@ regeneration, exact exported-fixture oracle checks, the pair after retirement, upgrade proof, full corpus and final hosted acceptance remain required before merge. No Canon reader is counted as merged retirement yet; no project-wide percentage follows from this bounded work. + +The exact seven TIFF byte vectors used by the public-reader tests are now +exported and independently replayed through pinned ExifTool, in both display +and numeric modes. All 14 native invocations exit successfully and all seven +sets of asserted values/omissions agree. These corrected successful fixtures +have no maker-note offset warning; the warning in the earlier scratch layout +is retained as historical evidence only. Full duplicate selection and native +warning output remain outside these public-reader assertions. + +The final display corpus pair at runtime `3d0353f4` covers all 4,238 files and +518,919 native tags. Both builds have 468,087 correct rows, VALUE 420, MISSING +12,240, RENAME 22 and EXTRA 1,560. There are no meaningful per-file raw-output +changes, lost matched keys, input-hash changes or parse/crash failures. The +supervisor retains exit 1 for native's existing zero-byte FujiFilmISPro.jpg +diagnostic; both OxiDex processes exit 0 for that file. Elapsed time: 281.730 +seconds. This demonstrates a producer migration without changing corpus output. + +A separate immutable CLI triple comparison uses the seven exact fixtures and +three real files, limited to source-defined AFInfo2 fields plus RawDataOffset. +It records eight display and 25 numeric-form improvements with zero scoped +regressions. Fourteen unchanged observations are native Unknown fields +requested with `-u`, which the current OxiDex CLI does not expose; these are +not silently counted as successful extraction. Native warning text remains +outside that CLI comparison. + +The first official regeneration attempt at `62535db7` completed its tier-1 +generation and independent checks, but failed the write-set guard because the +coordinator edited this plan and CI configuration while it ran. No declared +artifact changed. This is an invalid complete-run attempt, not an accepted +regeneration. Commit those edits, freeze the tree and rerun both tiers before +claiming the pipeline passes; the failed log and snapshots are retained. From d6111d24aaecc62fb3bf045b38958ed9b6877e59 Mon Sep 17 00:00:00 2001 From: swackhamer Date: Sun, 13 Sep 2026 10:38:03 -0500 Subject: [PATCH 10/12] Record Canon retirement acceptance and plan shared read-write generation --- docs/AUTOGENERATION-PLAN.md | 66 +++++++++++++++++++++-- docs/AUTOGENERATION-PROGRESS.md | 22 ++++++++ docs/reference/afinfo2-production-plan.md | 47 +++++++++++++--- 3 files changed, 124 insertions(+), 11 deletions(-) diff --git a/docs/AUTOGENERATION-PLAN.md b/docs/AUTOGENERATION-PLAN.md index d570a41db..653de245e 100644 --- a/docs/AUTOGENERATION-PLAN.md +++ b/docs/AUTOGENERATION-PLAN.md @@ -8,7 +8,7 @@ They do not override the goals or progress rules here. See the [working scoreboard](AUTOGENERATION-PROGRESS.md) for the current milestone, measured baseline, completed checks and remaining work. -## The goal +## The goal and the current phase A change to ExifTool's tag definitions should flow into OxiDex by regeneration, without someone retyping the tag name, byte location, camera-selection rule or @@ -19,15 +19,73 @@ reading numbers, evaluating expressions and decrypting blocks. Tag-specific knowledge must come from the pinned ExifTool source. **Moving a hard-coded tag rule into a generator or a shared helper does not count as automating it.** -The target is the tag-specific metadata extraction behavior of pinned ExifTool -13.59 across its formats. The 4,238-file corpus is a test population, not a -way to exclude unexercised formats from the goal. +The target is tag-specific reading and writing behavior of pinned ExifTool +13.59 across its formats. Native read-only tags have no required write path; +native writable tags need a separately verified one. The 4,238-file read +corpus is a test population, not a way to exclude unexercised behavior. + +The current implementation batches primarily migrate reading. Completing those +batches does not establish generated write support. Shared source definitions +must account for both directions now, while reader and writer execution are +migrated and verified separately. Write-side inventory and costing are in +progress; the earlier reading estimate is not a full read/write estimate. We are finished when all tag-specific rules in that target come from that source, the required behavior works, the replaced manual rules are removed, and an upgrade demonstrates this. Unsupported behavior and unmeasured areas remain unfinished; they cannot disappear from the denominator. +## How it works, in plain English + +The inputs are ExifTool's actual Perl tables and executable rules, not comments +or documentation. We load the pinned source's real table structures and +translate supported conditions, conversions and processing behavior into +shared Rust definitions and operations. Unsupported Perl behavior remains +explicitly unfinished; this is not an arbitrary Perl-to-Rust translator. + +A sample is not required to generate a supported tag definition. Samples test +execution. Independent checks compare generated definitions with the native +source, constructed files exercise rare rules and boundaries, and real files +test complete parsing. Track generated, independently verified and exercised +in a file separately. Absence from the corpus must not erase a native tag. + +## Reading and writing share definitions, with separate execution checks + +Capture the tag identity, type, placement, permissions, forward conversions and +available inverse conversions together. Avoid duplicating tag knowledge in a +second generator for writing. Missing inverse behavior is an explicit refusal, +not permission to guess a reversal of the read conversion. + +OxiDex already has write paths for JPEG, PNG, PDF and walkable TIFF-based +files, including some RAW containers, in `src/core/operations.rs`. These are +container routes, not evidence that every native-writable tag is supported. +The generated Canon reader migration does not activate generated writing. + +The source dump captures facts such as `Writable`, `PrintConvInv`, +`ValueConvInv` and write-processing declarations. Inventory which facts are +retained, translated, consumed by a writer, manual, unsupported or unclassified +before counting progress. Writing needs encoding, insertion/deletion, placement +and offset handling as well as preservation of unrelated metadata and file data. + +In parallel with the current reader acceptance, audit the existing writers +and the shared schema. Select a small native-writable family already understood +by the reader, generate its write-specific rules through the same source model, +and retire the duplicate manual rules only after actual write/read-back proof. +Do not create another vendor-specific translator or defer writer needs until +the entire reading migration is finished. + +Track native writable rules accounted for, generated write rules, manual write +rules and unsupported/unclassified behavior separately from reading. Test +create/update/delete on disposable copies, read the results with pinned +ExifTool, and verify preservation of unmodified data. A source-change rehearsal +must update write behavior without tag-specific Rust/Python edits. A successful +read census proves none of these write requirements. + +The JPEG matrix is a useful starting instrument. Its committed report is dated +August 12; it is not a refreshed measurement of this candidate or all formats. +Native read-only tags are explicitly ineligible for writing, not implementation +gaps. No current generated-writing percentage has been established. + ## Where we are now | Work | Verified state | What it means | diff --git a/docs/AUTOGENERATION-PROGRESS.md b/docs/AUTOGENERATION-PROGRESS.md index 293e2c280..edd9041ea 100644 --- a/docs/AUTOGENERATION-PROGRESS.md +++ b/docs/AUTOGENERATION-PROGRESS.md @@ -4,6 +4,28 @@ This is the working scoreboard for [the plan](AUTOGENERATION-PLAN.md). ## Current checkpoint — September 13 +The current Canon AFInfo2/AFInfo3 reader retirement is committed and published +at `ff1e364c`, but not merged. It deletes the shared manual reader arm, eight +private sequence offsets, two parent IDs and the private 20-value mode enum. +The generated reader now owns these two parent routes. Independent source +review, exact public-fixture native replay, the 67-file bounded pair, the full +4,238-file pair and all 32-artifact regeneration checks pass. Full corpus +output is unchanged; the bounded pair adds two correct rows and removes 40 +extras. The 140 focused Python tests execute the actual artifact mutation +checks with no skips. A copied native field rename also reaches actual output after official +regeneration with no handwritten tag-rule edit. Final hosted acceptance remains. See the +[production plan](reference/afinfo2-production-plan.md) for failed attempts, +exact evidence, and remaining old-AFInfo/CanonRaw scope. + +These results concern reading. The plan now explicitly shares source definitions +between reading and writing, while tracking write execution and preservation +separately. Two independent source audits are identifying existing writer rules +and missing shared-schema facts. No generated-writing percentage or full +read/write delivery estimate exists yet. The earlier 97.3% reading conformance +and route-disabled measurements say nothing about write support. + +### Latest merged checkpoint + Latest combined Canon definitions: **AFInfo 14/14 and AFInfo2 16/16**, both independently verified after formatting. Across all eight serial tables, emitted alternatives rise **106 -> 122** and omissions fall **26 -> 10**. diff --git a/docs/reference/afinfo2-production-plan.md b/docs/reference/afinfo2-production-plan.md index c25962b0e..446daeb8a 100644 --- a/docs/reference/afinfo2-production-plan.md +++ b/docs/reference/afinfo2-production-plan.md @@ -93,11 +93,12 @@ codegen tests and full Clippy pass. Retain that failed attempt in the evidence. Independent artifact review also found that supported processor selection was not yet verified and that old AFInfo's unsupported geometry validator was incorrectly emitted as executable. The generator now leaves that distinct -edge explicitly unwalked. The processor-oracle repair, full official -regeneration, exact exported-fixture oracle checks, the pair after retirement, -upgrade proof, full corpus and final hosted acceptance remain required before -merge. No Canon reader is counted as merged retirement yet; no project-wide -percentage follows from this bounded work. +edge explicitly unwalked. The processor-oracle repair is integrated at +`62535db7`; independent whole-batch source review accepts `ff1e364c`. Exact +exported-fixture checks, the pair after retirement and full corpus are accepted +below. Final hosted acceptance remains required before merge. +No Canon reader is counted as merged retirement yet; no project-wide percentage +follows from this bounded work. The exact seven TIFF byte vectors used by the public-reader tests are now exported and independently replayed through pinned ExifTool, in both display @@ -127,5 +128,37 @@ The first official regeneration attempt at `62535db7` completed its tier-1 generation and independent checks, but failed the write-set guard because the coordinator edited this plan and CI configuration while it ran. No declared artifact changed. This is an invalid complete-run attempt, not an accepted -regeneration. Commit those edits, freeze the tree and rerun both tiers before -claiming the pipeline passes; the failed log and snapshots are retained. +regeneration. The failed log and snapshots are retained. + +The frozen retry at `ff1e364c` passes both tiers in 91.262 seconds: all 32 +declared artifacts reproduce with zero changes. The actual generated parent +mutation test also runs with fresh canonical oracle inputs: all 140 focused +Python tests pass with zero skips, including processor, validation and stale +artifact rejection checks. + +The final bounded pair uses immutable `3d0353f4` after removing the manual +reader: 53 real and 14 constructed inputs, 12,758 native tags, correct rows +11,841 -> 11,843 and extras 43 -> 3. VALUE remains seven. There are no process +failures, changed inputs, lost matched keys or real-file raw-output changes. +The six constructed-file changes match the earlier checkpoint; retaining the +manual reader is therefore not what produced those accepted results. + +This delivery migrates reading. Generated writing and its create/update/delete +preservation tests remain a separate workstream in the main plan. + +The supported source-change proof passes. A copied pinned Canon.pm changes +only AFInfo2 entry 8's name from AFAreaWidths to UpgradeAFAreaWidths. Official +regeneration of all 32 artifacts succeeds in 247.561 seconds; four generated +files change (the serial name and associated source/dump provenance). Clippy +passes, then the isolated proof build completes in 56.001 seconds. Proof +commit `28f3f0dc` changes no handwritten Rust/Python or carrier code. + +On the exact II/MM public fixtures in display and numeric modes, the regenerated +binary changes that output key, preserves its value and matches the independently +executed modified native source. All other meaningful output is unchanged. +The original unfiltered comparison is retained: two first reads changed the +filesystem FileAccessDate. The accepted comparison excludes only that established +volatile key; it does not ignore any tag-value discrepancy. This proves the +selected supported name change, not arbitrary Perl translation or a release +upgrade. Evidence is under the batch's `source-upgrade-proof/` and the preserved +native `BATCH/source-upgrade-proof/native-proof/` subdirectory. From 78fb7921eff77a63a970aed2e54a24d20fa2c623 Mon Sep 17 00:00:00 2001 From: swackhamer Date: Sun, 13 Sep 2026 10:52:51 -0500 Subject: [PATCH 11/12] Correct the legacy AFInfo2 fixture size and assert native rejection --- src/parsers/tiff/makernotes/canon.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/parsers/tiff/makernotes/canon.rs b/src/parsers/tiff/makernotes/canon.rs index aef55ea07..d7e6f09ea 100644 --- a/src/parsers/tiff/makernotes/canon.rs +++ b/src/parsers/tiff/makernotes/canon.rs @@ -8740,15 +8740,14 @@ mod tests { assert_eq!(result.get("Canon:Canon_AFInfo_0x000b"), None); } - /// Mirrors the AFInfo2 record of - /// `/tmp/oxidex-exiftool-cache/combined-samples/Canon1DmkIII.jpg`, whose - /// `exiftool -s` output is `AFAreaMode: Single-point AF`, `NumAFPoints: 45`, + /// Exercises the AFInfo2 values of the Canon1DmkIII.jpg sample, whose + /// pinned ExifTool output is `AFAreaMode: Single-point AF`, `NumAFPoints: 45`, /// `AFImageWidth: 3888`, `AFImageHeight: 2592`, `AFPointsInFocus: 13`. #[test] fn test_parse_af_info2_array() { let n = 45usize; let mut af_info2: Vec = vec![ - 0, // key 0: AFInfoSize + 0, // key 0: deliberately invalid AFInfoSize, repaired below 2, // key 1: AFAreaMode -> 'Single-point AF' 45, // key 2: NumAFPoints 45, // key 3: ValidAFPoints @@ -8763,6 +8762,15 @@ mod tests { af_info2.extend(std::iter::repeat_n(-554i16, n)); // key 11: AFAreaYPositions af_info2.extend_from_slice(&[0x2000, 0x0000, 0x0000]); // key 12: bit 13 set + // Native Canon::Main validates the child's stored byte size before + // reading AFInfo2. The old fixture left this zero and only passed + // because the retired manual decoder skipped that validation. + let invalid = canon_makernote_with_short_array(0x0026, &af_info2); + let rejected = parse_canon_makernote_impl(&invalid, ByteOrder::LittleEndian).unwrap(); + assert!(!rejected.contains_key("Canon:AFAreaMode")); + assert!(!rejected.contains_key("Canon:NumAFPoints")); + + af_info2[0] = i16::try_from(af_info2.len() * 2).unwrap(); let data = canon_makernote_with_short_array(0x0026, &af_info2); let result = parse_canon_makernote_impl(&data, ByteOrder::LittleEndian).unwrap(); From 2630ded84a96c2973d3130cad2a26ce3629f01c7 Mon Sep 17 00:00:00 2001 From: swackhamer Date: Sun, 13 Sep 2026 10:54:25 -0500 Subject: [PATCH 12/12] Plan generated read-write behavior and reproducible version rehearsals --- docs/AUTOGENERATION-PLAN.md | 30 +++++++-- docs/AUTOGENERATION-PROGRESS.md | 20 ++++++ docs/reference/afinfo2-production-plan.md | 12 ++++ docs/reference/read-write-version-plan.md | 78 +++++++++++++++++++++++ 4 files changed, 136 insertions(+), 4 deletions(-) create mode 100644 docs/reference/read-write-version-plan.md diff --git a/docs/AUTOGENERATION-PLAN.md b/docs/AUTOGENERATION-PLAN.md index 653de245e..0cf91ff34 100644 --- a/docs/AUTOGENERATION-PLAN.md +++ b/docs/AUTOGENERATION-PLAN.md @@ -19,10 +19,14 @@ reading numbers, evaluating expressions and decrypting blocks. Tag-specific knowledge must come from the pinned ExifTool source. **Moving a hard-coded tag rule into a generator or a shared helper does not count as automating it.** -The target is tag-specific reading and writing behavior of pinned ExifTool -13.59 across its formats. Native read-only tags have no required write path; -native writable tags need a separately verified one. The 4,238-file read -corpus is a test population, not a way to exclude unexercised behavior. +The target is tag-specific reading and writing behavior derived from ExifTool's +Perl source across its formats and upstream releases. The current working pin +is 13.59; it does not limit the requested version scope. Every generated build +must conform to the native release it came from. Native read-only tags have no +required write path; native writable tags need a separately verified one. The +4,238-file read corpus is a test population, not a way to exclude unexercised +behavior or untested versions. The [read/write and version execution plan](reference/read-write-version-plan.md) +records the expanded finish line, first writer pilot and periodic release tests. The current implementation batches primarily migrate reading. Completing those batches does not establish generated write support. Shared source definitions @@ -86,6 +90,24 @@ August 12; it is not a refreshed measurement of this candidate or all formats. Native read-only tags are explicitly ineligible for writing, not implementation gaps. No current generated-writing percentage has been established. +## Version upgrades and periodic tests + +Use the same compiler and shared readers/writers to regenerate for each selected +native release. Keep an immutable upstream release catalog, reproducible random +seeds and a per-version result ledger. The existing bump promotion compares both +binaries against the newer oracle; add a separate non-promoting rehearsal that +regenerates both releases and checks each against its own native read/write +behavior. Selection or successful generation alone is not conformance. + +Once that runner passes its own tests, exercise a randomly selected distinct +release pair after three relevant merged batches or one week, whichever comes +first. The existing hourly continuation records this cadence without launching +a heavy test on every wake-up. Persist failures, manual interventions, +unsupported/untested releases and unexercised behavior. Replay known failures +alongside new selections. Random samples discover gaps; they cannot certify +all ExifTool versions. No full read/write, all-version completion estimate has +been established. + ## Where we are now | Work | Verified state | What it means | diff --git a/docs/AUTOGENERATION-PROGRESS.md b/docs/AUTOGENERATION-PROGRESS.md index edd9041ea..7d35e6094 100644 --- a/docs/AUTOGENERATION-PROGRESS.md +++ b/docs/AUTOGENERATION-PROGRESS.md @@ -24,6 +24,26 @@ and missing shared-schema facts. No generated-writing percentage or full read/write delivery estimate exists yet. The earlier 97.3% reading conformance and route-disabled measurements say nothing about write support. +The ready retirement PR is #760. Its first hosted test run found an old +synthetic AFInfo2 record with a zero size field and positive output assertions. +Pinned native complete-carrier replay proved those assertions invalid. +The repair at `78fb7921` keeps a rejection case and supplies the correct size +for the positive case. The full local Cargo unit/integration/doc invocation +passes: 5,993 passes, zero failures, 124 ignored, 150.641 seconds. The required +hosted checks are being rerun; do not count this as a merged retirement yet. + +The write audits now identify the first vertical pilot as Exif::Main +HostComputer `0x013c`, whose read behavior has no omitted conversion. They +also identify lost placement/inverse/delete/create source facts and unresolved +writer/checker implementation binding as prerequisites. A dedicated native +write-fact capture is in implementation; no generated writer has been enabled. +The existing bump tool is a promotion comparison against the newer oracle, +not per-version native proof. Seeded release-plan/journal primitives and an +independent native write contract are being prepared in parallel. The +[version plan](reference/read-write-version-plan.md) accounts for both +read/write execution and all requested release scope without promoting random +samples to exhaustive evidence. + ### Latest merged checkpoint Latest combined Canon definitions: **AFInfo 14/14 and AFInfo2 16/16**, both diff --git a/docs/reference/afinfo2-production-plan.md b/docs/reference/afinfo2-production-plan.md index 446daeb8a..af9b16d38 100644 --- a/docs/reference/afinfo2-production-plan.md +++ b/docs/reference/afinfo2-production-plan.md @@ -162,3 +162,15 @@ volatile key; it does not ignore any tag-value discrepancy. This proves the selected supported name change, not arbitrary Perl translation or a release upgrade. Evidence is under the batch's `source-upgrade-proof/` and the preserved native `BATCH/source-upgrade-proof/native-proof/` subdirectory. + +PR #760 opened ready at `d6111d24`. Its first hosted Build & Test job failed +`test_parse_af_info2_array`: the old constructed record declared AFInfoSize=0 +but expected child values. Native complete-carrier replay confirms rejection +at zero and all seven original asserted values at the correct 382-byte size. +Commit `78fb7921` retains explicit zero-size rejection assertions and repairs +the positive fixture; production code is unchanged. Formatting and Clippy +pass. The full local `cargo test --all-features --no-fail-fast` invocation +passes in 150.641 seconds: 5,993 passes across unit/integration/doc summaries, +zero failures and 124 ignored tests. The attempted local nextest invocation +could not run because that executable is not installed; its failed attempt is +retained. Hosted nextest and all required checks must pass on the final head. diff --git a/docs/reference/read-write-version-plan.md b/docs/reference/read-write-version-plan.md new file mode 100644 index 000000000..c2b1132bd --- /dev/null +++ b/docs/reference/read-write-version-plan.md @@ -0,0 +1,78 @@ +# Generated reading, writing and ExifTool upgrades + +Updated September 13, 2026. This records the expanded read/write and version +objective alongside the [main plan](../AUTOGENERATION-PLAN.md). + +## Finish line + +OxiDex should derive all tag-specific reading and native-writable tag behavior +from the selected ExifTool Perl source. Shared Rust mechanisms still perform +file access, arithmetic, encoding and safe file changes. A tag-specific rule +retyped into Python or Rust is unfinished automation. + +The version used to build generated definitions is the version used to check +those definitions and their behavior. ExifTool 13.59 is the current working pin, +not a limit on the requested version scope. Keep an explicit upstream release +catalog and per-version results. Missing source, unsupported semantics, +unexercised behaviors and failed versions remain visible. Native read-only +fields are explicitly ineligible for writes, not failed writer implementations. + +## Work in order, with independent tasks in parallel + +1. Finish Canon AFInfo2/AFInfo3 reader retirement through PR #760. Preserve the + failed legacy synthetic test, correct its invalid size field with native + evidence, run full tests and merge only after the required checks pass. +2. Capture one complete source model for both directions. Retain permissions, + actual writer/checker functions, placement, forward and inverse conversions, + validation, insertion/deletion and ordering rules. Keep unknown property + values and distinguish absent, undefined, zero and empty. Preserve existing + read admission while expanding write facts. +3. Prove the first complete generated writer using HostComputer `0x013c` and the + generic Exif::Main scalar rule class. Generate its actual physical IFD0 + placement and write type; use the existing JPEG/TIFF surgical mechanisms. + Verify insert, update, growth, shrinkage and deletion against native ExifTool + in both byte orders, preserving unrelated metadata and image/file payload. + A copied native name/type/placement change must propagate without another + hand-written rule. Retire the replaced manual lookup after proof. +4. Expand shared capabilities and migrate eligible read/write families through + them. Publish manual rules removed and remaining unsupported behavior for + each direction; table/line counts alone are not completion. +5. Add a non-promoting version-rehearsal runner around existing generation and + isolated build mechanisms. The existing promotion bump compares both builds + against the new oracle, so it is not proof of each version's native behavior. + Regenerate/build BOTH selected versions and compare each with ITS native + reader/writer, using the same declared fixtures and corpus where applicable. +6. Exercise upgrades repeatedly, then close the remaining historical-version + and semantic inventory. Random tests are a discovery tool; they cannot + certify untested versions or behaviors. + +## Reproducible random version rehearsals + +Snapshot the actual official release tags with immutable commit/archive +identities. Pick two distinct releases from that catalog with a persisted seed, +record selection before work starts, then test older-to-newer regeneration. +Do not silently restrict selection to versions already known to pass. Missing +native prerequisites or unsupported old syntax must be recorded as failures or +explicit unsupported scope, not skipped out of the success denominator. + +Run after every three relevant merged generator/reader/writer batches or one +week since the last rehearsal, whichever comes first, once the runner exists +and passes its own tests. +Keep a persisted counter and last run identity so interruption cannot reset the +cadence or accidentally launch duplicate runs. The existing hourly continuation now records this cadence; it must finish +and validate the runner before launching expensive rehearsals. Do not create +a duplicate schedule or run a rehearsal on every heartbeat. +All heavy work uses its host queue/lock and survives disconnects. + +For each version record native source/interpreter/capability identity, generated +artifact hashes, clean code revision, binary hash, fixture hashes, all commands +and exits, and raw native/OxiDex outputs. Compare create/update/delete plus +unmodified-data preservation separately from reads. Record every manual edit +needed to make the upgrade work. Successful automation requires zero +tag-specific edits; unsupported semantics require shared implementation work. + +Use prior failures as fixed regression cases alongside new random selections. +Report the complete catalog population, tested versions/pairs, failures, +untested releases and unexercised read/write behaviors. Keep corpus attribution, +read conformance, write conformance and source-rule automation as separate +measurements. No exact completion date follows from the current partial data.