diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4bfa536d4..c8ec4c821 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -374,6 +374,16 @@ jobs: -- --ignored --exact --nocapture | tee "$RUNNER_TEMP/word-native-replay.log" # Cargo also succeeds when a mistyped exact selector runs no tests. grep -F 'test result: ok. 1 passed; 0 failed;' "$RUNNER_TEMP/word-native-replay.log" + - name: Compare generated serial tables with pinned native processor + env: + EXIFTOOL_PERL: perl + run: | + set -euo pipefail + export OXIDEX_PINNED_EXIFTOOL="$(dirname "$EXIFTOOL")" + cargo test --all-features --test serial_directory_native \ + real_audio_serial_tables_replay_pinned_native_callbacks \ + -- --ignored --exact --nocapture | tee "$RUNNER_TEMP/serial-native-replay.log" + grep -F 'test result: ok. 1 passed; 0 failed;' "$RUNNER_TEMP/serial-native-replay.log" - name: Verify C header is up-to-date run: | cbindgen --config cbindgen.toml --crate oxidex --output api/oxidex.h.tmp @@ -569,6 +579,8 @@ jobs: # the source-rebinding, complete-inventory and native replay cases. export OXIDEX_PINNED_EXIFTOOL="$ROOT" 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" 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/AUTOGENERATION-PLAN.md b/docs/AUTOGENERATION-PLAN.md index 5564118ca..380268f4b 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. | -| Dynamic serial-processor validation | Merged in PR #755 at `93d19e24`; all five hosted checks pass, including 706 native Python tests. | The probe validates native reads and callbacks. The next reviewed source inventory accounts for eight tables and 132 alternatives; shared Rust execution and manual-reader retirement still need proof. | +| Shared serial processor | Native probe and inventory merged in #755/#756. The next combined checkpoint compiles eight generated tables, accounts for 106 emitted alternatives and 26 omissions, and passes Real AudioV3/V4 native/Rust replay. | Final hosted acceptance and merge remain. A separate V4 draft replaces its manual 31-slot sequence; carrier/corpus acceptance is still required before counting retirement. | | 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. | | Sony enciphered recovery | Producer and independent verifier preserved; M4 review found five blockers; not landed | The draft still has a Sony-specific translation layer. Its review remains useful, but it is not the architecture target. | @@ -150,8 +150,9 @@ Canonical artifacts and ledgers were regenerated successfully with local Perl 1. Compile sequential fields and lengths that depend on earlier raw values from the native serial processor. The [native probe](reference/serial-processor-checkpoint.md) is merged in #755. The reviewed JSON inventory captures all eight selected - tables and preserves every refusal. A shared Rust emitter and reader are - staged separately; combined compilation and native replay remain open. + tables and preserves every refusal. The combined Rust emitter and reader + compile and pass native Real AudioV3/V4 replay. Complete their final hosted + gate, then validate the V4 carrier and remove its manual sequence. Account for every condition and conversion before reducing the final unsupported-child count from one to zero. 2. Resolve the four omitted parent rows using shared byte handling. An explicit diff --git a/docs/AUTOGENERATION-PROGRESS.md b/docs/AUTOGENERATION-PROGRESS.md index ce58c6ccc..cff219a31 100644 --- a/docs/AUTOGENERATION-PROGRESS.md +++ b/docs/AUTOGENERATION-PROGRESS.md @@ -46,9 +46,10 @@ and zero skips**, in 566.450 seconds. The initial threaded-Perl failure was reproduced locally and fixed; the eight focused probe tests pass on both threaded and non-threaded Perl 5.38.2. -The next reviewed source batch is committed on -`codex/ciff-opaque-native-integration-20260913`; its first published complete -inventory checkpoint is `37c32d58`. It accounts for all eight source-selected +The source batch merged in PR #756 as `eb700430` at 11:46 UTC. All five +hosted checks passed at `298917dd`, including 735 canonical Python tests, +zero failures and zero skips, in 653.244 seconds. Its first published complete +inventory checkpoint was `37c32d58`. It accounts for all eight source-selected serial tables: **130 entries, 132 alternatives, 115 clear at the source gate and 17 with explicit refusals**. One table also retains a priority-policy blocker. A complete processor grammar rejects changed executable behavior; @@ -62,13 +63,37 @@ ordering. Final output groups/conversions and actual image digest computation remain outside these probes' proof. Warnings are recorded separately from the chronological callback trace. The combined checkpoint at `66c430c6` passes **37 focused tests, zero failures and zero skips**, on both threaded and non-threaded Perl 5.38.2 -against pinned ExifTool 13.59. Full hosted checks remain required for merge. - -The next delivery is one shared serial reader and source-driven Rust emitter, -followed by native/Rust replay. Their worker commits are staged separately and -still need combined compilation and review. No production route is enabled; -**one unsupported Canon child processor, four omitted parent rows and zero -Canon manual readers retired** remain the production status. +against pinned ExifTool 13.59. The full hosted checks then passed for #756. + +The shared serial reader and Rust emitter are now combined on +`codex/shared-serial-integration-20260913`, with pipeline checkpoint `9693ef9e`. +The [runtime checkpoint](reference/serial-runtime-checkpoint.md) records the +proof and its limits. All eight tables remain accounted for: **106 emitted +alternatives and 26 explicit omissions**, with zero independent verifier +mismatches. This is stricter than the source-only 115/17 split because the +emitter also refuses unproved runtime formats and missing-member conditions. +Four tables clear the definition gate; native/Rust replay currently covers +Real AudioV3 and AudioV4 only. + +All 64 focused Python tests pass with zero failures and zero skips in +65.023 seconds. The full `cargo clippy --all-features -- -D warnings` check +and formatting check pass. All 13 shared-reader unit tests pass. The explicit +native/Rust test passes +its ten cases on both threaded and non-threaded Perl 5.38.2, using the actual +generated tables. Official regeneration passes across 32 declared artifacts; +all generated Rust is unchanged. The expression ledger initially records the +local absolute invocation paths. Re-running its producer with the portable +invocation reproduces the committed ledger exactly. All 607 expressions agree +on 16,789 applicable comparisons; 14 inputs are inapplicable. + +This checkpoint still needs its final hosted gate and merge. No production +route is enabled; **one unsupported Canon child processor, four omitted parent +rows and zero Canon manual readers retired** remain the production status. +The Real AudioV4 retirement draft is separately committed and published at +`118afac7`. Source review accepts its narrow behavior, but it still needs a +fresh Rust build, native output comparison, full-corpus pair and upgrade-flow +proof. Its metadata occurrence API retains the existing group-0/group-2 +limitations; native warning output and V3/V5 activation also remain unfinished. The first additional retirement candidate is Real AudioV4's manually specified 31-entry sequence. It uses the same native serial processor and can prove reuse diff --git a/docs/UPGRADE-NEXT-STEPS.md b/docs/UPGRADE-NEXT-STEPS.md index c64550b92..17614bd10 100644 --- a/docs/UPGRADE-NEXT-STEPS.md +++ b/docs/UPGRADE-NEXT-STEPS.md @@ -19,7 +19,7 @@ populations. They are implementation history, not work to restart. | Priority | Remaining work | State | Completion evidence | | --- | --- | --- | --- | -| 1 | Extend the source inventory and complete the next shared directory capability | Sony Tag202a migration/retirement merged in #746; source inventory and artifact join merged in #749/#751. Directory validation and nine word tables merged in #753/#754; canonical regeneration and all final gates pass. One child processor and four parent rows remain unsupported. The serial native probe merged in #755 with all five checks passing. The next source inventory accounts for eight tables / 132 alternatives; staged Rust emission and reading still need combined validation. Real AudioV4 is the next shared-capability retirement candidate. | Classify runtime/manual rules against the recorded inventory; resolve named processor blockers, verify both carriers, then retire the duplicate readers. Details in the [scoreboard](./AUTOGENERATION-PROGRESS.md). | +| 1 | Extend the source inventory and complete the next shared directory capability | Sony Tag202a migration/retirement merged in #746; source inventory and artifact join merged in #749/#751. Directory validation and nine word tables merged in #753/#754; canonical regeneration and all final gates pass. One child processor and four parent rows remain unsupported. Serial probe/inventory merged in #755/#756. Combined Rust emission/reading passes native V3/V4 replay; eight tables account for 106 emitted alternatives and 26 omissions. Final hosted gate is pending. Real AudioV4 retirement is published separately and still needs carrier/corpus/upgrade-flow acceptance. | Classify runtime/manual rules against the recorded inventory; resolve named processor blockers, verify both carriers, then retire the duplicate readers. Details in the [scoreboard](./AUTOGENERATION-PROGRESS.md). | | 2 | Continue generated EXIF directory migration | IFD1, InteropIFD and ExifIFD E-2 landed; Claude's handoff reserves E-3, fallback retirement, IFD0 and numeric-coercion work | Refresh Claude's ownership before taking work; preserve occurrence behavior and measure each activation against its own control | | 3 | Join producer and upgrade accounting | Source-to-artifact join merged; runtime and manually maintained rule classification remain open | Generated facts, manual rules, unsupported rules and actual execution remain distinct; ordinary source changes need no new tag rules | | 4 | Preserve Sony/Nikon producer recovery evidence | Sony plain landed; enciphered/encrypted recovery preserved and unlanded | Recovery is labeled maintenance; no numbering-only reconstruction or duplicate per-vendor interpreter is promoted as the target | diff --git a/docs/reference/serial-layout-inventory-afinfo.json b/docs/reference/serial-layout-inventory-afinfo.json index ac9c8f540..46b8dbe30 100644 --- a/docs/reference/serial-layout-inventory-afinfo.json +++ b/docs/reference/serial-layout-inventory-afinfo.json @@ -1 +1 @@ -{"entries":[{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"NumAFPoints","native":{"Name":"NumAFPoints"},"refusals":[]}],"serial_index":0},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"ValidAFPoints","native":{"Name":"ValidAFPoints","Notes":"number of AF points valid in the following information"},"refusals":[]}],"serial_index":1},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{"2":"Image"},"name":"CanonImageWidth","native":{"Groups":{"2":"Image"},"Name":"CanonImageWidth"},"refusals":[]}],"serial_index":2},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{"2":"Image"},"name":"CanonImageHeight","native":{"Groups":{"2":"Image"},"Name":"CanonImageHeight"},"refusals":[]}],"serial_index":3},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"AFImageWidth","native":{"Name":"AFImageWidth","Notes":"size of image in AF coordinates"},"refusals":[]}],"serial_index":4},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"AFImageHeight","native":{"Name":"AFImageHeight","_shorthand":true},"refusals":[]}],"serial_index":5},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"AFAreaWidth","native":{"Name":"AFAreaWidth","_shorthand":true},"refusals":[]}],"serial_index":6},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"AFAreaHeight","native":{"Name":"AFAreaHeight","_shorthand":true},"refusals":[]}],"serial_index":7},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":0},"format":"int16s","format_source":"field"},"groups":{},"name":"AFAreaXPositions","native":{"Format":"int16s[$val{0}]","Name":"AFAreaXPositions"},"refusals":[]}],"serial_index":8},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":0},"format":"int16s","format_source":"field"},"groups":{},"name":"AFAreaYPositions","native":{"Format":"int16s[$val{0}]","Name":"AFAreaYPositions"},"refusals":[]}],"serial_index":9},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"expression":"Image::ExifTool::DecodeBits($val, undef, 16)","kind":"native_expr"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"add":15,"divisor":16,"kind":"floor_div_prior_raw_value","serial_index":0},"format":"int16s","format_source":"field"},"groups":{},"name":"AFPointsInFocus","native":{"Format":"int16s[int(($val{0}+15)/16)]","Name":"AFPointsInFocus","PrintConv":{"expr":"Image::ExifTool::DecodeBits($val, undef, 16)","kind":"expr"}},"refusals":["serial_decode_bits_words"]}],"serial_index":10},{"alternatives":[{"condition":{"compiled":"Cond::And(&Cond::MemberRegex { member: \"Model\", pattern: \"EOS\", ignore_case: false, negate: true }, &Cond::Or(&Cond::MemberTruthy { member: \"AFInfoCount\", negate: true }, &Cond::MemberCmp { member: \"AFInfoCount\", op: CmpOp::Ne, value: 36 }))","kind":"shared_cond","missing_member":"empty_string","source":"\n $$self{Model} !~ /EOS/ and\n (not $$self{AFInfoCount} or $$self{AFInfoCount} != 36)\n "},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"PrimaryAFPoint","native":{"Condition":"\n $$self{Model} !~ /EOS/ and\n (not $$self{AFInfoCount} or $$self{AFInfoCount} != 36)\n ","Name":"PrimaryAFPoint"},"refusals":[]},{"condition":{"compiled":"Cond::MemberRegex { member: \"Model\", pattern: \"EOS\", ignore_case: false, negate: true }","kind":"shared_cond","missing_member":"empty_string","source":"$$self{Model} !~ /EOS/"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":8},"format":"int16u","format_source":"field"},"groups":{},"name":"Canon_AFInfo_0x000b","native":{"Condition":"$$self{Model} !~ /EOS/","Format":"int16u[8]","Name":"Canon_AFInfo_0x000b","Unknown":"1"},"refusals":[]}],"serial_index":11},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"PrimaryAFPoint","native":{"Name":"PrimaryAFPoint","_shorthand":true},"refusals":[]}],"serial_index":12}],"gate_a":{"blocked_by":[["serial_decode_bits_words",1]]},"kind":"native_serial_layout_inventory","module":"Canon","processor":{"condition_missing_member":"native_perl_empty_string","effects_in_order":["serial processor package","temporary unknown option","unknown generation suppression","serial verbose callback","table default format","serial GetTagInfo","dynamic count evaluation","remainder string","field length","bounded read","raw value storage","subdirectory branch","tag reporting","serial cursor advance","unknown option restore","unknown generation cleanup"],"name":"Image::ExifTool::Canon::ProcessSerialData","prior_value_domain":"raw_read_value_before_conversion_or_reporting","source_body_sha256":"ab5cb31e06a991a02f5569f1c008303c0505d28caf7ae2ec044aeede9c9f999c","source_file":"Image/ExifTool/Canon.pm","source_sha256":"d22b110e7b2e1af2d6e59bb97db97a04ffc2bfd1ba7d078d380ae60fbc70bbba"},"runtime_status":"inventory_only_no_reader_or_route","table":"AFInfo","table_facts":{"default_format":"int16u","documentary":{"NOTES":"\n Auto-focus information used by many older Canon models. The values in this\n record are sequential, and some have variable sizes based on the value of\n NumAFPoints (which may be 1,5,7,9,15,45 or 53). The AFArea coordinates are\n given in a system where the image has dimensions given by AFImageWidth and\n AFImageHeight, and 0,0 is the image center. The direction of the Y axis\n depends on the camera model, with positive Y upwards for EOS models, but\n apparently downwards for PowerShot models.\n "},"groups":{"0":"MakerNotes","2":"Camera"},"native_table_sha256":"45e5f32aa56326d5043149994fcd71ea8056d86af70f73855a64d82065eb7b47","unmodeled":{},"variables":{"ID_LABEL":"Sequence"}},"version":1} +{"entries":[{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"NumAFPoints","native":{"Name":"NumAFPoints"},"refusals":[]}],"serial_index":0,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"ValidAFPoints","native":{"Name":"ValidAFPoints","Notes":"number of AF points valid in the following information"},"refusals":[]}],"serial_index":1,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{"2":"Image"},"name":"CanonImageWidth","native":{"Groups":{"2":"Image"},"Name":"CanonImageWidth"},"refusals":[]}],"serial_index":2,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{"2":"Image"},"name":"CanonImageHeight","native":{"Groups":{"2":"Image"},"Name":"CanonImageHeight"},"refusals":[]}],"serial_index":3,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"AFImageWidth","native":{"Name":"AFImageWidth","Notes":"size of image in AF coordinates"},"refusals":[]}],"serial_index":4,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"AFImageHeight","native":{"Name":"AFImageHeight","_shorthand":true},"refusals":[]}],"serial_index":5,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"AFAreaWidth","native":{"Name":"AFAreaWidth","_shorthand":true},"refusals":[]}],"serial_index":6,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"AFAreaHeight","native":{"Name":"AFAreaHeight","_shorthand":true},"refusals":[]}],"serial_index":7,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":0},"format":"int16s","format_source":"field"},"groups":{},"name":"AFAreaXPositions","native":{"Format":"int16s[$val{0}]","Name":"AFAreaXPositions"},"refusals":[]}],"serial_index":8,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":0},"format":"int16s","format_source":"field"},"groups":{},"name":"AFAreaYPositions","native":{"Format":"int16s[$val{0}]","Name":"AFAreaYPositions"},"refusals":[]}],"serial_index":9,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"expression":"Image::ExifTool::DecodeBits($val, undef, 16)","kind":"native_expr"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"add":15,"divisor":16,"kind":"floor_div_prior_raw_value","serial_index":0},"format":"int16s","format_source":"field"},"groups":{},"name":"AFPointsInFocus","native":{"Format":"int16s[int(($val{0}+15)/16)]","Name":"AFPointsInFocus","PrintConv":{"expr":"Image::ExifTool::DecodeBits($val, undef, 16)","kind":"expr"}},"refusals":["serial_decode_bits_words"]}],"serial_index":10,"variant":false},{"alternatives":[{"condition":{"compiled":"Cond::And(&Cond::MemberRegex { member: \"Model\", pattern: \"EOS\", ignore_case: false, negate: true }, &Cond::Or(&Cond::MemberTruthy { member: \"AFInfoCount\", negate: true }, &Cond::MemberCmp { member: \"AFInfoCount\", op: CmpOp::Ne, value: 36 }))","kind":"shared_cond","missing_member":"empty_string","source":"\n $$self{Model} !~ /EOS/ and\n (not $$self{AFInfoCount} or $$self{AFInfoCount} != 36)\n "},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"PrimaryAFPoint","native":{"Condition":"\n $$self{Model} !~ /EOS/ and\n (not $$self{AFInfoCount} or $$self{AFInfoCount} != 36)\n ","Name":"PrimaryAFPoint"},"refusals":[]},{"condition":{"compiled":"Cond::MemberRegex { member: \"Model\", pattern: \"EOS\", ignore_case: false, negate: true }","kind":"shared_cond","missing_member":"empty_string","source":"$$self{Model} !~ /EOS/"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":8},"format":"int16u","format_source":"field"},"groups":{},"name":"Canon_AFInfo_0x000b","native":{"Condition":"$$self{Model} !~ /EOS/","Format":"int16u[8]","Name":"Canon_AFInfo_0x000b","Unknown":"1"},"refusals":[]}],"serial_index":11,"variant":true},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"PrimaryAFPoint","native":{"Name":"PrimaryAFPoint","_shorthand":true},"refusals":[]}],"serial_index":12,"variant":false}],"gate_a":{"blocked_by":[["serial_decode_bits_words",1]]},"kind":"native_serial_layout_inventory","module":"Canon","processor":{"condition_missing_member":"native_perl_empty_string","effects_in_order":["serial processor package","temporary unknown option","unknown generation suppression","serial verbose callback","table default format","serial GetTagInfo","dynamic count evaluation","remainder string","field length","bounded read","raw value storage","subdirectory branch","tag reporting","serial cursor advance","unknown option restore","unknown generation cleanup"],"name":"Image::ExifTool::Canon::ProcessSerialData","prior_value_domain":"raw_read_value_before_conversion_or_reporting","source_body_sha256":"ab5cb31e06a991a02f5569f1c008303c0505d28caf7ae2ec044aeede9c9f999c","source_file":"Image/ExifTool/Canon.pm","source_sha256":"d22b110e7b2e1af2d6e59bb97db97a04ffc2bfd1ba7d078d380ae60fbc70bbba"},"runtime_status":"inventory_only_no_reader_or_route","table":"AFInfo","table_facts":{"default_format":"int16u","documentary":{"NOTES":"\n Auto-focus information used by many older Canon models. The values in this\n record are sequential, and some have variable sizes based on the value of\n NumAFPoints (which may be 1,5,7,9,15,45 or 53). The AFArea coordinates are\n given in a system where the image has dimensions given by AFImageWidth and\n AFImageHeight, and 0,0 is the image center. The direction of the Y axis\n depends on the camera model, with positive Y upwards for EOS models, but\n apparently downwards for PowerShot models.\n "},"groups":{"0":"MakerNotes","2":"Camera"},"native_table_sha256":"45e5f32aa56326d5043149994fcd71ea8056d86af70f73855a64d82065eb7b47","unmodeled":{},"variables":{"ID_LABEL":"Sequence"}},"version":1} diff --git a/docs/reference/serial-layout-inventory-processserialdata.json b/docs/reference/serial-layout-inventory-processserialdata.json index 20ab9ce13..47941f5b1 100644 --- a/docs/reference/serial-layout-inventory-processserialdata.json +++ b/docs/reference/serial-layout-inventory-processserialdata.json @@ -1 +1 @@ -{"kind":"native_serial_layout_population","runtime_status":"inventory_only_no_reader_or_route","selector":{"processor_suffix":"::ProcessSerialData"},"source_modules_sha256":"8124f4081c4e651c9595353af48a90376c873383310b9e8d8f556fcc81b15f71","summary":{"compiled_alternatives":132,"compiled_entries":130,"descriptor_compiled_tables":8,"descriptor_refused_tables":0,"empty_selected_tables":0,"native_alternatives":132,"native_entries":130,"row_gate_blocked_by":[["serial_decode_bits_words",3],["serial_print_conv",2],["serial_print_conv_expr",8],["serial_raw_conv",2],["serial_row_shape",2],["serial_value_conv",4]],"row_gate_clear_alternatives":115,"row_gate_clear_tables":4,"row_gate_refused_alternatives":17,"selected_tables":8,"table_gate_blocked_by":[["serial_table_priority",1]],"table_gate_blocked_tables":1},"tables":[{"descriptor":{"entries":[{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"NumAFPoints","native":{"Name":"NumAFPoints"},"refusals":[]}],"serial_index":0},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"ValidAFPoints","native":{"Name":"ValidAFPoints","Notes":"number of AF points valid in the following information"},"refusals":[]}],"serial_index":1},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{"2":"Image"},"name":"CanonImageWidth","native":{"Groups":{"2":"Image"},"Name":"CanonImageWidth"},"refusals":[]}],"serial_index":2},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{"2":"Image"},"name":"CanonImageHeight","native":{"Groups":{"2":"Image"},"Name":"CanonImageHeight"},"refusals":[]}],"serial_index":3},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"AFImageWidth","native":{"Name":"AFImageWidth","Notes":"size of image in AF coordinates"},"refusals":[]}],"serial_index":4},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"AFImageHeight","native":{"Name":"AFImageHeight","_shorthand":true},"refusals":[]}],"serial_index":5},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"AFAreaWidth","native":{"Name":"AFAreaWidth","_shorthand":true},"refusals":[]}],"serial_index":6},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"AFAreaHeight","native":{"Name":"AFAreaHeight","_shorthand":true},"refusals":[]}],"serial_index":7},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":0},"format":"int16s","format_source":"field"},"groups":{},"name":"AFAreaXPositions","native":{"Format":"int16s[$val{0}]","Name":"AFAreaXPositions"},"refusals":[]}],"serial_index":8},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":0},"format":"int16s","format_source":"field"},"groups":{},"name":"AFAreaYPositions","native":{"Format":"int16s[$val{0}]","Name":"AFAreaYPositions"},"refusals":[]}],"serial_index":9},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"expression":"Image::ExifTool::DecodeBits($val, undef, 16)","kind":"native_expr"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"add":15,"divisor":16,"kind":"floor_div_prior_raw_value","serial_index":0},"format":"int16s","format_source":"field"},"groups":{},"name":"AFPointsInFocus","native":{"Format":"int16s[int(($val{0}+15)/16)]","Name":"AFPointsInFocus","PrintConv":{"expr":"Image::ExifTool::DecodeBits($val, undef, 16)","kind":"expr"}},"refusals":["serial_decode_bits_words"]}],"serial_index":10},{"alternatives":[{"condition":{"compiled":"Cond::And(&Cond::MemberRegex { member: \"Model\", pattern: \"EOS\", ignore_case: false, negate: true }, &Cond::Or(&Cond::MemberTruthy { member: \"AFInfoCount\", negate: true }, &Cond::MemberCmp { member: \"AFInfoCount\", op: CmpOp::Ne, value: 36 }))","kind":"shared_cond","missing_member":"empty_string","source":"\n $$self{Model} !~ /EOS/ and\n (not $$self{AFInfoCount} or $$self{AFInfoCount} != 36)\n "},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"PrimaryAFPoint","native":{"Condition":"\n $$self{Model} !~ /EOS/ and\n (not $$self{AFInfoCount} or $$self{AFInfoCount} != 36)\n ","Name":"PrimaryAFPoint"},"refusals":[]},{"condition":{"compiled":"Cond::MemberRegex { member: \"Model\", pattern: \"EOS\", ignore_case: false, negate: true }","kind":"shared_cond","missing_member":"empty_string","source":"$$self{Model} !~ /EOS/"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":8},"format":"int16u","format_source":"field"},"groups":{},"name":"Canon_AFInfo_0x000b","native":{"Condition":"$$self{Model} !~ /EOS/","Format":"int16u[8]","Name":"Canon_AFInfo_0x000b","Unknown":"1"},"refusals":[]}],"serial_index":11},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"PrimaryAFPoint","native":{"Name":"PrimaryAFPoint","_shorthand":true},"refusals":[]}],"serial_index":12}],"gate_a":{"blocked_by":[["serial_decode_bits_words",1]]},"kind":"native_serial_layout_inventory","module":"Canon","processor":{"condition_missing_member":"native_perl_empty_string","effects_in_order":["serial processor package","temporary unknown option","unknown generation suppression","serial verbose callback","table default format","serial GetTagInfo","dynamic count evaluation","remainder string","field length","bounded read","raw value storage","subdirectory branch","tag reporting","serial cursor advance","unknown option restore","unknown generation cleanup"],"name":"Image::ExifTool::Canon::ProcessSerialData","prior_value_domain":"raw_read_value_before_conversion_or_reporting","source_body_sha256":"ab5cb31e06a991a02f5569f1c008303c0505d28caf7ae2ec044aeede9c9f999c","source_file":"Image/ExifTool/Canon.pm","source_sha256":"d22b110e7b2e1af2d6e59bb97db97a04ffc2bfd1ba7d078d380ae60fbc70bbba"},"runtime_status":"inventory_only_no_reader_or_route","table":"AFInfo","table_facts":{"default_format":"int16u","documentary":{"NOTES":"\n Auto-focus information used by many older Canon models. The values in this\n record are sequential, and some have variable sizes based on the value of\n NumAFPoints (which may be 1,5,7,9,15,45 or 53). The AFArea coordinates are\n given in a system where the image has dimensions given by AFImageWidth and\n AFImageHeight, and 0,0 is the image center. The direction of the Y axis\n depends on the camera model, with positive Y upwards for EOS models, but\n apparently downwards for PowerShot models.\n "},"groups":{"0":"MakerNotes","2":"Camera"},"native_table_sha256":"45e5f32aa56326d5043149994fcd71ea8056d86af70f73855a64d82065eb7b47","unmodeled":{},"variables":{"ID_LABEL":"Sequence"}},"version":1},"module":"Canon","native_alternatives":14,"native_entries":13,"outcome":"descriptor_compiled","table":"AFInfo"},{"descriptor":{"entries":[{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"AFInfoSize","native":{"Name":"AFInfoSize","Unknown":"1"},"refusals":[]}],"serial_index":0},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"native_print_conv","source":{"directives":null,"kind":"enum","map":{"0":"Off (Manual Focus)","1":"AF Point Expansion (surround)","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","2":"Single-point AF","20":"Flexible Zone AF 2","21":"Flexible Zone AF 3","22":"Whole Area AF","4":"Auto","5":"Face Detect AF","6":"Face + Tracking","7":"Zone AF","8":"AF Point Expansion (4 point)","9":"Spot AF"}}},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"AFAreaMode","native":{"Name":"AFAreaMode","PrintConv":{"directives":null,"kind":"enum","map":{"0":"Off (Manual Focus)","1":"AF Point Expansion (surround)","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","2":"Single-point AF","20":"Flexible Zone AF 2","21":"Flexible Zone AF 3","22":"Whole Area AF","4":"Auto","5":"Face Detect AF","6":"Face + Tracking","7":"Zone AF","8":"AF Point Expansion (4 point)","9":"Spot AF"}}},"refusals":["serial_print_conv"]}],"serial_index":1},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"NumAFPoints","native":{"Name":"NumAFPoints","RawConv":{"expr":"$$self{NumAFPoints} = $val","kind":"expr"}},"refusals":["serial_raw_conv"]}],"serial_index":2},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"ValidAFPoints","native":{"Name":"ValidAFPoints","Notes":"number of AF points valid in the following information"},"refusals":[]}],"serial_index":3},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{"2":"Image"},"name":"CanonImageWidth","native":{"Groups":{"2":"Image"},"Name":"CanonImageWidth"},"refusals":[]}],"serial_index":4},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{"2":"Image"},"name":"CanonImageHeight","native":{"Groups":{"2":"Image"},"Name":"CanonImageHeight"},"refusals":[]}],"serial_index":5},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"AFImageWidth","native":{"Name":"AFImageWidth","Notes":"size of image in AF coordinates"},"refusals":[]}],"serial_index":6},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"AFImageHeight","native":{"Name":"AFImageHeight","_shorthand":true},"refusals":[]}],"serial_index":7},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":2},"format":"int16s","format_source":"field"},"groups":{},"name":"AFAreaWidths","native":{"Format":"int16s[$val{2}]","Name":"AFAreaWidths"},"refusals":[]}],"serial_index":8},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":2},"format":"int16s","format_source":"field"},"groups":{},"name":"AFAreaHeights","native":{"Format":"int16s[$val{2}]","Name":"AFAreaHeights"},"refusals":[]}],"serial_index":9},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":2},"format":"int16s","format_source":"field"},"groups":{},"name":"AFAreaXPositions","native":{"Format":"int16s[$val{2}]","Name":"AFAreaXPositions"},"refusals":[]}],"serial_index":10},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":2},"format":"int16s","format_source":"field"},"groups":{},"name":"AFAreaYPositions","native":{"Format":"int16s[$val{2}]","Name":"AFAreaYPositions"},"refusals":[]}],"serial_index":11},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"expression":"Image::ExifTool::DecodeBits($val, undef, 16)","kind":"native_expr"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"add":15,"divisor":16,"kind":"floor_div_prior_raw_value","serial_index":2},"format":"int16s","format_source":"field"},"groups":{},"name":"AFPointsInFocus","native":{"Format":"int16s[int(($val{2}+15)/16)]","Name":"AFPointsInFocus","PrintConv":{"expr":"Image::ExifTool::DecodeBits($val, undef, 16)","kind":"expr"}},"refusals":["serial_decode_bits_words"]}],"serial_index":12},{"alternatives":[{"condition":{"compiled":"Cond::MemberRegex { member: \"Model\", pattern: \"EOS\", ignore_case: false, negate: false }","kind":"shared_cond","missing_member":"empty_string","source":"$$self{Model} =~ /EOS/"},"conversion":{"expression":"Image::ExifTool::DecodeBits($val, undef, 16)","kind":"native_expr"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"add":15,"divisor":16,"kind":"floor_div_prior_raw_value","serial_index":2},"format":"int16s","format_source":"field"},"groups":{},"name":"AFPointsSelected","native":{"Condition":"$$self{Model} =~ /EOS/","Format":"int16s[int(($val{2}+15)/16)]","Name":"AFPointsSelected","PrintConv":{"expr":"Image::ExifTool::DecodeBits($val, undef, 16)","kind":"expr"}},"refusals":["serial_decode_bits_words"]},{"name":"Canon_AFInfo2_0x000d","native":{"Format":"int16s[int(($val{2}+15)/16)+1]","Name":"Canon_AFInfo2_0x000d","Unknown":"1"},"native_refusal":"serial Format count expression is outside the closed prior-value grammar","refusals":["serial_row_shape"]}],"serial_index":13},{"alternatives":[{"condition":{"compiled":"Cond::And(&Cond::MemberRegex { member: \"Model\", pattern: \"EOS\", ignore_case: false, negate: true }, &Cond::MemberTruthy { member: \"AFInfo3\", negate: true })","kind":"shared_cond","missing_member":"empty_string","source":"$$self{Model} !~ /EOS/ and not $$self{AFInfo3}"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"PrimaryAFPoint","native":{"Condition":"$$self{Model} !~ /EOS/ and not $$self{AFInfo3}","Name":"PrimaryAFPoint"},"refusals":[]}],"serial_index":14}],"gate_a":{"blocked_by":[["serial_decode_bits_words",2],["serial_print_conv",1],["serial_raw_conv",1],["serial_row_shape",1]]},"kind":"native_serial_layout_inventory","module":"Canon","processor":{"condition_missing_member":"native_perl_empty_string","effects_in_order":["serial processor package","temporary unknown option","unknown generation suppression","serial verbose callback","table default format","serial GetTagInfo","dynamic count evaluation","remainder string","field length","bounded read","raw value storage","subdirectory branch","tag reporting","serial cursor advance","unknown option restore","unknown generation cleanup"],"name":"Image::ExifTool::Canon::ProcessSerialData","prior_value_domain":"raw_read_value_before_conversion_or_reporting","source_body_sha256":"ab5cb31e06a991a02f5569f1c008303c0505d28caf7ae2ec044aeede9c9f999c","source_file":"Image/ExifTool/Canon.pm","source_sha256":"d22b110e7b2e1af2d6e59bb97db97a04ffc2bfd1ba7d078d380ae60fbc70bbba"},"runtime_status":"inventory_only_no_reader_or_route","table":"AFInfo2","table_facts":{"default_format":"int16u","documentary":{"NOTES":"\n Newer version of the AFInfo record containing much of the same information\n (and coordinate confusion) as the older version. In this record, NumAFPoints\n may be 7, 9, 11, 19, 31, 45 or 61, depending on the camera model.\n "},"groups":{"0":"MakerNotes","2":"Camera"},"native_table_sha256":"4da030bfec2ba6648fbb3d9783ae56ecf293fa440d701ba3dfe63600a1d8c9db","unmodeled":{},"variables":{"ID_LABEL":"Sequence"}},"version":1},"module":"Canon","native_alternatives":16,"native_entries":15,"outcome":"descriptor_compiled","table":"AFInfo2"},{"descriptor":{"entries":[{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"field"},"groups":{},"name":"Channels","native":{"Format":"int16u","Name":"Channels"},"refusals":[]}],"serial_index":0},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":3},"format":"int16u","format_source":"field"},"groups":{},"name":"Unknown","native":{"Format":"int16u[3]","Name":"Unknown","Unknown":"1"},"refusals":[]}],"serial_index":1},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"field"},"groups":{},"name":"BytesPerMinute","native":{"Format":"int16u","Name":"BytesPerMinute"},"refusals":[]}],"serial_index":2},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"field"},"groups":{},"name":"AudioBytes","native":{"Format":"int32u","Name":"AudioBytes"},"refusals":[]}],"serial_index":3},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int8u","format_source":"table_default"},"groups":{},"name":"TitleLen","native":{"Name":"TitleLen","Unknown":"1"},"refusals":[]}],"serial_index":4},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":4},"format":"string","format_source":"field"},"groups":{},"name":"Title","native":{"Format":"string[$val{4}]","Name":"Title"},"refusals":[]}],"serial_index":5},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int8u","format_source":"table_default"},"groups":{},"name":"ArtistLen","native":{"Name":"ArtistLen","Unknown":"1"},"refusals":[]}],"serial_index":6},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":6},"format":"string","format_source":"field"},"groups":{"2":"Author"},"name":"Artist","native":{"Format":"string[$val{6}]","Groups":{"2":"Author"},"Name":"Artist"},"refusals":[]}],"serial_index":7},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int8u","format_source":"table_default"},"groups":{},"name":"CopyrightLen","native":{"Name":"CopyrightLen","Unknown":"1"},"refusals":[]}],"serial_index":8},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":8},"format":"string","format_source":"field"},"groups":{"2":"Author"},"name":"Copyright","native":{"Format":"string[$val{8}]","Groups":{"2":"Author"},"Name":"Copyright"},"refusals":[]}],"serial_index":9},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int8u","format_source":"table_default"},"groups":{},"name":"CommentLen","native":{"Name":"CommentLen","Unknown":"1"},"refusals":[]}],"serial_index":10},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":10},"format":"string","format_source":"field"},"groups":{},"name":"Comment","native":{"Format":"string[$val{10}]","Name":"Comment"},"refusals":[]}],"serial_index":11}],"gate_a":{"blocked_by":[]},"kind":"native_serial_layout_inventory","module":"Real","processor":{"condition_missing_member":"native_perl_empty_string","effects_in_order":["serial processor package","temporary unknown option","unknown generation suppression","serial verbose callback","table default format","serial GetTagInfo","dynamic count evaluation","remainder string","field length","bounded read","raw value storage","subdirectory branch","tag reporting","serial cursor advance","unknown option restore","unknown generation cleanup"],"name":"Image::ExifTool::Canon::ProcessSerialData","prior_value_domain":"raw_read_value_before_conversion_or_reporting","source_body_sha256":"ab5cb31e06a991a02f5569f1c008303c0505d28caf7ae2ec044aeede9c9f999c","source_file":"Image/ExifTool/Canon.pm","source_sha256":"d22b110e7b2e1af2d6e59bb97db97a04ffc2bfd1ba7d078d380ae60fbc70bbba"},"runtime_status":"inventory_only_no_reader_or_route","table":"AudioV3","table_facts":{"default_format":"int8u","documentary":{},"groups":{"1":"Real-RA3","2":"Audio"},"native_table_sha256":"5b947891835e07639d2640349868fa05c9c5b5f55f7e1c835224a42d3a7ab975","unmodeled":{},"variables":{"ID_LABEL":"Sequence"}},"version":1},"module":"Real","native_alternatives":12,"native_entries":12,"outcome":"descriptor_compiled","table":"AudioV3"},{"descriptor":{"entries":[{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":4},"format":"undef","format_source":"field"},"groups":{},"name":"FourCC1","native":{"Format":"undef[4]","Name":"FourCC1","Unknown":"1"},"refusals":[]}],"serial_index":0},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"field"},"groups":{},"name":"AudioFileSize","native":{"Format":"int32u","Name":"AudioFileSize","Unknown":"1"},"refusals":[]}],"serial_index":1},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"Version2","native":{"Name":"Version2","Unknown":"1"},"refusals":[]}],"serial_index":2},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"field"},"groups":{},"name":"HeaderSize","native":{"Format":"int32u","Name":"HeaderSize","Unknown":"1"},"refusals":[]}],"serial_index":3},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"CodecFlavorID","native":{"Name":"CodecFlavorID","Unknown":"1"},"refusals":[]}],"serial_index":4},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"field"},"groups":{},"name":"CodedFrameSize","native":{"Format":"int32u","Name":"CodedFrameSize","Unknown":"1"},"refusals":[]}],"serial_index":5},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"field"},"groups":{},"name":"AudioBytes","native":{"Format":"int32u","Name":"AudioBytes"},"refusals":[]}],"serial_index":6},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"field"},"groups":{},"name":"BytesPerMinute","native":{"Format":"int32u","Name":"BytesPerMinute"},"refusals":[]}],"serial_index":7},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"field"},"groups":{},"name":"Unknown","native":{"Format":"int32u","Name":"Unknown","Unknown":"1"},"refusals":[]}],"serial_index":8},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"SubPacketH","native":{"Name":"SubPacketH","Unknown":"1"},"refusals":[]}],"serial_index":9},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"AudioFrameSize","native":{"Name":"AudioFrameSize","_shorthand":true},"refusals":[]}],"serial_index":10},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"SubPacketSize","native":{"Name":"SubPacketSize","Unknown":"1"},"refusals":[]}],"serial_index":11},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"Unknown","native":{"Name":"Unknown","Unknown":"1"},"refusals":[]}],"serial_index":12},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"SampleRate","native":{"Name":"SampleRate","_shorthand":true},"refusals":[]}],"serial_index":13},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"Unknown","native":{"Name":"Unknown","Unknown":"1"},"refusals":[]}],"serial_index":14},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"BitsPerSample","native":{"Name":"BitsPerSample","_shorthand":true},"refusals":[]}],"serial_index":15},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"Channels","native":{"Name":"Channels","_shorthand":true},"refusals":[]}],"serial_index":16},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int8u","format_source":"field"},"groups":{},"name":"FourCC2Len","native":{"Format":"int8u","Name":"FourCC2Len","Unknown":"1"},"refusals":[]}],"serial_index":17},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":4},"format":"undef","format_source":"field"},"groups":{},"name":"FourCC2","native":{"Format":"undef[4]","Name":"FourCC2","Unknown":"1"},"refusals":[]}],"serial_index":18},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int8u","format_source":"field"},"groups":{},"name":"FourCC3Len","native":{"Format":"int8u","Name":"FourCC3Len","Unknown":"1"},"refusals":[]}],"serial_index":19},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":4},"format":"undef","format_source":"field"},"groups":{},"name":"FourCC3","native":{"Format":"undef[4]","Name":"FourCC3","Unknown":"1"},"refusals":[]}],"serial_index":20},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int8u","format_source":"field"},"groups":{},"name":"Unknown","native":{"Format":"int8u","Name":"Unknown","Unknown":"1"},"refusals":[]}],"serial_index":21},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"Unknown","native":{"Name":"Unknown","Unknown":"1"},"refusals":[]}],"serial_index":22},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int8u","format_source":"field"},"groups":{},"name":"TitleLen","native":{"Format":"int8u","Name":"TitleLen","Unknown":"1"},"refusals":[]}],"serial_index":23},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":23},"format":"string","format_source":"field"},"groups":{},"name":"Title","native":{"Format":"string[$val{23}]","Name":"Title"},"refusals":[]}],"serial_index":24},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int8u","format_source":"field"},"groups":{},"name":"ArtistLen","native":{"Format":"int8u","Name":"ArtistLen","Unknown":"1"},"refusals":[]}],"serial_index":25},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":25},"format":"string","format_source":"field"},"groups":{"2":"Author"},"name":"Artist","native":{"Format":"string[$val{25}]","Groups":{"2":"Author"},"Name":"Artist"},"refusals":[]}],"serial_index":26},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int8u","format_source":"field"},"groups":{},"name":"CopyrightLen","native":{"Format":"int8u","Name":"CopyrightLen","Unknown":"1"},"refusals":[]}],"serial_index":27},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":27},"format":"string","format_source":"field"},"groups":{"2":"Author"},"name":"Copyright","native":{"Format":"string[$val{27}]","Groups":{"2":"Author"},"Name":"Copyright"},"refusals":[]}],"serial_index":28},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int8u","format_source":"field"},"groups":{},"name":"CommentLen","native":{"Format":"int8u","Name":"CommentLen","Unknown":"1"},"refusals":[]}],"serial_index":29},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":29},"format":"string","format_source":"field"},"groups":{},"name":"Comment","native":{"Format":"string[$val{29}]","Name":"Comment"},"refusals":[]}],"serial_index":30}],"gate_a":{"blocked_by":[]},"kind":"native_serial_layout_inventory","module":"Real","processor":{"condition_missing_member":"native_perl_empty_string","effects_in_order":["serial processor package","temporary unknown option","unknown generation suppression","serial verbose callback","table default format","serial GetTagInfo","dynamic count evaluation","remainder string","field length","bounded read","raw value storage","subdirectory branch","tag reporting","serial cursor advance","unknown option restore","unknown generation cleanup"],"name":"Image::ExifTool::Canon::ProcessSerialData","prior_value_domain":"raw_read_value_before_conversion_or_reporting","source_body_sha256":"ab5cb31e06a991a02f5569f1c008303c0505d28caf7ae2ec044aeede9c9f999c","source_file":"Image/ExifTool/Canon.pm","source_sha256":"d22b110e7b2e1af2d6e59bb97db97a04ffc2bfd1ba7d078d380ae60fbc70bbba"},"runtime_status":"inventory_only_no_reader_or_route","table":"AudioV4","table_facts":{"default_format":"int16u","documentary":{},"groups":{"1":"Real-RA4","2":"Audio"},"native_table_sha256":"9fa6f8407b6678ed007722829d5e2974b9d5034fbd6b61f22fbebeaf6d0d1065","unmodeled":{},"variables":{"ID_LABEL":"Sequence"}},"version":1},"module":"Real","native_alternatives":31,"native_entries":31,"outcome":"descriptor_compiled","table":"AudioV4"},{"descriptor":{"entries":[{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":4},"format":"undef","format_source":"field"},"groups":{},"name":"FourCC1","native":{"Format":"undef[4]","Name":"FourCC1","Unknown":"1"},"refusals":[]}],"serial_index":0},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"field"},"groups":{},"name":"AudioFileSize","native":{"Format":"int32u","Name":"AudioFileSize","Unknown":"1"},"refusals":[]}],"serial_index":1},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"Version2","native":{"Name":"Version2","Unknown":"1"},"refusals":[]}],"serial_index":2},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"field"},"groups":{},"name":"HeaderSize","native":{"Format":"int32u","Name":"HeaderSize","Unknown":"1"},"refusals":[]}],"serial_index":3},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"CodecFlavorID","native":{"Name":"CodecFlavorID","Unknown":"1"},"refusals":[]}],"serial_index":4},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"field"},"groups":{},"name":"CodedFrameSize","native":{"Format":"int32u","Name":"CodedFrameSize","Unknown":"1"},"refusals":[]}],"serial_index":5},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"field"},"groups":{},"name":"AudioBytes","native":{"Format":"int32u","Name":"AudioBytes"},"refusals":[]}],"serial_index":6},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"field"},"groups":{},"name":"BytesPerMinute","native":{"Format":"int32u","Name":"BytesPerMinute"},"refusals":[]}],"serial_index":7},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"field"},"groups":{},"name":"Unknown","native":{"Format":"int32u","Name":"Unknown","Unknown":"1"},"refusals":[]}],"serial_index":8},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"SubPacketH","native":{"Name":"SubPacketH","Unknown":"1"},"refusals":[]}],"serial_index":9},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"FrameSize","native":{"Name":"FrameSize","Unknown":"1"},"refusals":[]}],"serial_index":10},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"SubPacketSize","native":{"Name":"SubPacketSize","Unknown":"1"},"refusals":[]}],"serial_index":11},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"SampleRate","native":{"Name":"SampleRate","_shorthand":true},"refusals":[]}],"serial_index":12},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"SampleRate2","native":{"Name":"SampleRate2","Unknown":"1"},"refusals":[]}],"serial_index":13},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"field"},"groups":{},"name":"BitsPerSample","native":{"Format":"int32u","Name":"BitsPerSample"},"refusals":[]}],"serial_index":14},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"Channels","native":{"Name":"Channels","_shorthand":true},"refusals":[]}],"serial_index":15},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"field"},"groups":{},"name":"Genr","native":{"Format":"int32u","Name":"Genr","Unknown":"1"},"refusals":[]}],"serial_index":16},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":4},"format":"undef","format_source":"field"},"groups":{},"name":"FourCC3","native":{"Format":"undef[4]","Name":"FourCC3","Unknown":"1"},"refusals":[]}],"serial_index":17}],"gate_a":{"blocked_by":[]},"kind":"native_serial_layout_inventory","module":"Real","processor":{"condition_missing_member":"native_perl_empty_string","effects_in_order":["serial processor package","temporary unknown option","unknown generation suppression","serial verbose callback","table default format","serial GetTagInfo","dynamic count evaluation","remainder string","field length","bounded read","raw value storage","subdirectory branch","tag reporting","serial cursor advance","unknown option restore","unknown generation cleanup"],"name":"Image::ExifTool::Canon::ProcessSerialData","prior_value_domain":"raw_read_value_before_conversion_or_reporting","source_body_sha256":"ab5cb31e06a991a02f5569f1c008303c0505d28caf7ae2ec044aeede9c9f999c","source_file":"Image/ExifTool/Canon.pm","source_sha256":"d22b110e7b2e1af2d6e59bb97db97a04ffc2bfd1ba7d078d380ae60fbc70bbba"},"runtime_status":"inventory_only_no_reader_or_route","table":"AudioV5","table_facts":{"default_format":"int16u","documentary":{},"groups":{"1":"Real-RA5","2":"Audio"},"native_table_sha256":"41ab70aabcb492d0d251e4a4f32d1b1e6d7d008e7a7d791bf0fc9bd6294d41ce","unmodeled":{},"variables":{"ID_LABEL":"Sequence"}},"version":1},"module":"Real","native_alternatives":18,"native_entries":18,"outcome":"descriptor_compiled","table":"AudioV5"},{"descriptor":{"entries":[{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"TitleLen","native":{"Name":"TitleLen","Unknown":"1"},"refusals":[]}],"serial_index":0},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":0},"format":"string","format_source":"field"},"groups":{},"name":"Title","native":{"Format":"string[$val{0}]","Name":"Title"},"refusals":[]}],"serial_index":1},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"AuthorLen","native":{"Name":"AuthorLen","Unknown":"1"},"refusals":[]}],"serial_index":2},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":2},"format":"string","format_source":"field"},"groups":{"2":"Author"},"name":"Author","native":{"Format":"string[$val{2}]","Groups":{"2":"Author"},"Name":"Author"},"refusals":[]}],"serial_index":3},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"CopyrightLen","native":{"Name":"CopyrightLen","Unknown":"1"},"refusals":[]}],"serial_index":4},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":4},"format":"string","format_source":"field"},"groups":{"2":"Author"},"name":"Copyright","native":{"Format":"string[$val{4}]","Groups":{"2":"Author"},"Name":"Copyright"},"refusals":[]}],"serial_index":5},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"CommentLen","native":{"Name":"CommentLen","Unknown":"1"},"refusals":[]}],"serial_index":6},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":6},"format":"string","format_source":"field"},"groups":{},"name":"Comment","native":{"Format":"string[$val{6}]","Name":"Comment"},"refusals":[]}],"serial_index":7}],"gate_a":{"blocked_by":[]},"kind":"native_serial_layout_inventory","module":"Real","processor":{"condition_missing_member":"native_perl_empty_string","effects_in_order":["serial processor package","temporary unknown option","unknown generation suppression","serial verbose callback","table default format","serial GetTagInfo","dynamic count evaluation","remainder string","field length","bounded read","raw value storage","subdirectory branch","tag reporting","serial cursor advance","unknown option restore","unknown generation cleanup"],"name":"Image::ExifTool::Canon::ProcessSerialData","prior_value_domain":"raw_read_value_before_conversion_or_reporting","source_body_sha256":"ab5cb31e06a991a02f5569f1c008303c0505d28caf7ae2ec044aeede9c9f999c","source_file":"Image/ExifTool/Canon.pm","source_sha256":"d22b110e7b2e1af2d6e59bb97db97a04ffc2bfd1ba7d078d380ae60fbc70bbba"},"runtime_status":"inventory_only_no_reader_or_route","table":"ContentDescr","table_facts":{"default_format":"int16u","documentary":{},"groups":{"1":"Real-CONT","2":"Video"},"native_table_sha256":"919d184e2424472d43929633f512c18440494f7058142a82d5e5c0de02667c1c","unmodeled":{},"variables":{"ID_LABEL":"Sequence"}},"version":1},"module":"Real","native_alternatives":8,"native_entries":8,"outcome":"descriptor_compiled","table":"ContentDescr"},{"descriptor":{"entries":[{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"field"},"groups":{},"name":"StreamNumber","native":{"Format":"int16u","Name":"StreamNumber"},"refusals":[]}],"serial_index":0},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"expression":"ConvertBitrate($val)","kind":"native_expr"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"table_default"},"groups":{},"name":"StreamMaxBitrate","native":{"Name":"StreamMaxBitrate","PrintConv":{"expr":"ConvertBitrate($val)","kind":"expr"}},"refusals":["serial_print_conv_expr"]}],"serial_index":1},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"expression":"ConvertBitrate($val)","kind":"native_expr"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"table_default"},"groups":{},"name":"StreamAvgBitrate","native":{"Name":"StreamAvgBitrate","PrintConv":{"expr":"ConvertBitrate($val)","kind":"expr"}},"refusals":["serial_print_conv_expr"]}],"serial_index":2},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"table_default"},"groups":{},"name":"StreamMaxPacketSize","native":{"Name":"StreamMaxPacketSize"},"refusals":[]}],"serial_index":3},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"table_default"},"groups":{},"name":"StreamAvgPacketSize","native":{"Name":"StreamAvgPacketSize"},"refusals":[]}],"serial_index":4},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"table_default"},"groups":{},"name":"StreamStartTime","native":{"Name":"StreamStartTime"},"refusals":[]}],"serial_index":5},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"expression":"ConvertDuration($val)","kind":"native_expr"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"table_default"},"groups":{},"name":"StreamPreroll","native":{"Name":"StreamPreroll","PrintConv":{"expr":"ConvertDuration($val)","kind":"expr"},"ValueConv":{"expr":"$val / 1000","kind":"expr"}},"refusals":["serial_print_conv_expr","serial_value_conv"]}],"serial_index":6},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"expression":"ConvertDuration($val)","kind":"native_expr"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"table_default"},"groups":{},"name":"StreamDuration","native":{"Name":"StreamDuration","PrintConv":{"expr":"ConvertDuration($val)","kind":"expr"},"ValueConv":{"expr":"$val / 1000","kind":"expr"}},"refusals":["serial_print_conv_expr","serial_value_conv"]}],"serial_index":7},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int8u","format_source":"field"},"groups":{},"name":"StreamNameLen","native":{"Format":"int8u","Name":"StreamNameLen","Unknown":"1"},"refusals":[]}],"serial_index":8},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":8},"format":"string","format_source":"field"},"groups":{},"name":"StreamName","native":{"Format":"string[$val{8}]","Name":"StreamName"},"refusals":[]}],"serial_index":9},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int8u","format_source":"field"},"groups":{},"name":"StreamMimeLen","native":{"Format":"int8u","Name":"StreamMimeLen","Unknown":"1"},"refusals":[]}],"serial_index":10},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":10},"format":"string","format_source":"field"},"groups":{},"name":"StreamMimeType","native":{"Format":"string[$val{10}]","Name":"StreamMimeType","RawConv":{"expr":"$self->{RealStreamMime} = $val","kind":"expr"}},"refusals":["serial_raw_conv"]}],"serial_index":11},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"table_default"},"groups":{},"name":"FileInfoLen","native":{"Name":"FileInfoLen","Unknown":"1"},"refusals":[]}],"serial_index":12},{"alternatives":[{"condition":{"compiled":"Cond::MemberStrEq { member: \"RealStreamMime\", value: \"logical-fileinfo\", negate: false }","kind":"shared_cond","missing_member":"shared_default","source":"$self->{RealStreamMime} eq \"logical-fileinfo\""},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"table_default"},"groups":{},"name":"FileInfoLen2","native":{"Condition":"$self->{RealStreamMime} eq \"logical-fileinfo\"","Name":"FileInfoLen2","Unknown":"1"},"refusals":[]}],"serial_index":13},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"field"},"groups":{},"name":"FileInfoVersion","native":{"Format":"int16u","Name":"FileInfoVersion"},"refusals":[]}],"serial_index":14},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"field"},"groups":{},"name":"PhysicalStreams","native":{"Format":"int16u","Name":"PhysicalStreams","Unknown":"1"},"refusals":[]}],"serial_index":15},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"prior_raw_value","serial_index":15},"format":"int16u","format_source":"field"},"groups":{},"name":"PhysicalStreamNumbers","native":{"Format":"int16u[$val{15}]","Name":"PhysicalStreamNumbers","Unknown":"1"},"refusals":[]}],"serial_index":16},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"prior_raw_value","serial_index":15},"format":"int32u","format_source":"field"},"groups":{},"name":"DataOffsets","native":{"Format":"int32u[$val{15}]","Name":"DataOffsets","Unknown":"1"},"refusals":[]}],"serial_index":17},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"field"},"groups":{},"name":"NumRules","native":{"Format":"int16u","Name":"NumRules","Unknown":"1"},"refusals":[]}],"serial_index":18},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"prior_raw_value","serial_index":18},"format":"int16u","format_source":"field"},"groups":{},"name":"PhysicalStreamNumberMap","native":{"Format":"int16u[$val{18}]","Name":"PhysicalStreamNumberMap","Unknown":"1"},"refusals":[]}],"serial_index":19},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"field"},"groups":{},"name":"NumProperties","native":{"Format":"int16u","Name":"NumProperties","Unknown":"1"},"refusals":[]}],"serial_index":20},{"alternatives":[{"name":"FileInfoProperties","native":{"Format":"undef[$val{13}-$val{15}*6-$val{18}*2-12]","Name":"FileInfoProperties","SubDirectory":{"TagTable":"Image::ExifTool::Real::FileInfo"}},"native_refusal":"serial Format count expression is outside the closed prior-value grammar","refusals":["serial_row_shape"]}],"serial_index":21}],"gate_a":{"blocked_by":[["serial_print_conv_expr",4],["serial_raw_conv",1],["serial_row_shape",1],["serial_table_priority",1],["serial_value_conv",2]]},"kind":"native_serial_layout_inventory","module":"Real","processor":{"condition_missing_member":"native_perl_empty_string","effects_in_order":["serial processor package","temporary unknown option","unknown generation suppression","serial verbose callback","table default format","serial GetTagInfo","dynamic count evaluation","remainder string","field length","bounded read","raw value storage","subdirectory branch","tag reporting","serial cursor advance","unknown option restore","unknown generation cleanup"],"name":"Image::ExifTool::Canon::ProcessSerialData","prior_value_domain":"raw_read_value_before_conversion_or_reporting","source_body_sha256":"ab5cb31e06a991a02f5569f1c008303c0505d28caf7ae2ec044aeede9c9f999c","source_file":"Image/ExifTool/Canon.pm","source_sha256":"d22b110e7b2e1af2d6e59bb97db97a04ffc2bfd1ba7d078d380ae60fbc70bbba"},"runtime_status":"inventory_only_no_reader_or_route","table":"MediaProps","table_facts":{"default_format":"int32u","documentary":{},"groups":{"1":"Real-MDPR","2":"Video"},"native_table_sha256":"7f027a5be2dd62b4d6b9693f05782ad5dabff5e9b6bcaf47dc2db6d9cfd6923b","unmodeled":{"PRIORITY":"0"},"variables":{"ID_LABEL":"Sequence"}},"version":1},"module":"Real","native_alternatives":22,"native_entries":22,"outcome":"descriptor_compiled","table":"MediaProps"},{"descriptor":{"entries":[{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"expression":"ConvertBitrate($val)","kind":"native_expr"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"table_default"},"groups":{},"name":"MaxBitrate","native":{"Name":"MaxBitrate","PrintConv":{"expr":"ConvertBitrate($val)","kind":"expr"}},"refusals":["serial_print_conv_expr"]}],"serial_index":0},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"expression":"ConvertBitrate($val)","kind":"native_expr"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"table_default"},"groups":{},"name":"AvgBitrate","native":{"Name":"AvgBitrate","PrintConv":{"expr":"ConvertBitrate($val)","kind":"expr"}},"refusals":["serial_print_conv_expr"]}],"serial_index":1},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"table_default"},"groups":{},"name":"MaxPacketSize","native":{"Name":"MaxPacketSize","_shorthand":true},"refusals":[]}],"serial_index":2},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"table_default"},"groups":{},"name":"AvgPacketSize","native":{"Name":"AvgPacketSize","_shorthand":true},"refusals":[]}],"serial_index":3},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"table_default"},"groups":{},"name":"NumPackets","native":{"Name":"NumPackets","_shorthand":true},"refusals":[]}],"serial_index":4},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"expression":"ConvertDuration($val)","kind":"native_expr"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"table_default"},"groups":{},"name":"Duration","native":{"Name":"Duration","PrintConv":{"expr":"ConvertDuration($val)","kind":"expr"},"ValueConv":{"expr":"$val / 1000","kind":"expr"}},"refusals":["serial_print_conv_expr","serial_value_conv"]}],"serial_index":5},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"expression":"ConvertDuration($val)","kind":"native_expr"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"table_default"},"groups":{},"name":"Preroll","native":{"Name":"Preroll","PrintConv":{"expr":"ConvertDuration($val)","kind":"expr"},"ValueConv":{"expr":"$val / 1000","kind":"expr"}},"refusals":["serial_print_conv_expr","serial_value_conv"]}],"serial_index":6},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"table_default"},"groups":{},"name":"IndexOffset","native":{"Name":"IndexOffset","Unknown":"1"},"refusals":[]}],"serial_index":7},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"table_default"},"groups":{},"name":"DataOffset","native":{"Name":"DataOffset","Unknown":"1"},"refusals":[]}],"serial_index":8},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"field"},"groups":{},"name":"NumStreams","native":{"Format":"int16u","Name":"NumStreams"},"refusals":[]}],"serial_index":9},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"native_print_conv","source":{"directives":{"BITMASK":{"0":"Allow Recording","1":"Perfect Play","2":"Live","3":"Allow Download"}},"kind":"enum_partial","map":{}}},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"field"},"groups":{},"name":"Flags","native":{"Format":"int16u","Name":"Flags","PrintConv":{"directives":{"BITMASK":{"0":"Allow Recording","1":"Perfect Play","2":"Live","3":"Allow Download"}},"kind":"enum_partial","map":{}}},"refusals":["serial_print_conv"]}],"serial_index":10}],"gate_a":{"blocked_by":[["serial_print_conv",1],["serial_print_conv_expr",4],["serial_value_conv",2]]},"kind":"native_serial_layout_inventory","module":"Real","processor":{"condition_missing_member":"native_perl_empty_string","effects_in_order":["serial processor package","temporary unknown option","unknown generation suppression","serial verbose callback","table default format","serial GetTagInfo","dynamic count evaluation","remainder string","field length","bounded read","raw value storage","subdirectory branch","tag reporting","serial cursor advance","unknown option restore","unknown generation cleanup"],"name":"Image::ExifTool::Canon::ProcessSerialData","prior_value_domain":"raw_read_value_before_conversion_or_reporting","source_body_sha256":"ab5cb31e06a991a02f5569f1c008303c0505d28caf7ae2ec044aeede9c9f999c","source_file":"Image/ExifTool/Canon.pm","source_sha256":"d22b110e7b2e1af2d6e59bb97db97a04ffc2bfd1ba7d078d380ae60fbc70bbba"},"runtime_status":"inventory_only_no_reader_or_route","table":"Properties","table_facts":{"default_format":"int32u","documentary":{},"groups":{"1":"Real-PROP","2":"Video"},"native_table_sha256":"b0fc620c04edba01c0667497be39b4a5ff3753c9850e64a8ee7da1ef5fc6d003","unmodeled":{},"variables":{"ID_LABEL":"Sequence"}},"version":1},"module":"Real","native_alternatives":11,"native_entries":11,"outcome":"descriptor_compiled","table":"Properties"}],"version":1} +{"kind":"native_serial_layout_population","runtime_status":"inventory_only_no_reader_or_route","selector":{"processor_suffix":"::ProcessSerialData"},"source_modules_sha256":"8124f4081c4e651c9595353af48a90376c873383310b9e8d8f556fcc81b15f71","summary":{"compiled_alternatives":132,"compiled_entries":130,"descriptor_compiled_tables":8,"descriptor_refused_tables":0,"empty_selected_tables":0,"native_alternatives":132,"native_entries":130,"row_gate_blocked_by":[["serial_decode_bits_words",3],["serial_print_conv",2],["serial_print_conv_expr",8],["serial_raw_conv",2],["serial_row_shape",2],["serial_value_conv",4]],"row_gate_clear_alternatives":115,"row_gate_clear_tables":4,"row_gate_refused_alternatives":17,"selected_tables":8,"table_gate_blocked_by":[["serial_table_priority",1]],"table_gate_blocked_tables":1},"tables":[{"descriptor":{"entries":[{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"NumAFPoints","native":{"Name":"NumAFPoints"},"refusals":[]}],"serial_index":0,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"ValidAFPoints","native":{"Name":"ValidAFPoints","Notes":"number of AF points valid in the following information"},"refusals":[]}],"serial_index":1,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{"2":"Image"},"name":"CanonImageWidth","native":{"Groups":{"2":"Image"},"Name":"CanonImageWidth"},"refusals":[]}],"serial_index":2,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{"2":"Image"},"name":"CanonImageHeight","native":{"Groups":{"2":"Image"},"Name":"CanonImageHeight"},"refusals":[]}],"serial_index":3,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"AFImageWidth","native":{"Name":"AFImageWidth","Notes":"size of image in AF coordinates"},"refusals":[]}],"serial_index":4,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"AFImageHeight","native":{"Name":"AFImageHeight","_shorthand":true},"refusals":[]}],"serial_index":5,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"AFAreaWidth","native":{"Name":"AFAreaWidth","_shorthand":true},"refusals":[]}],"serial_index":6,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"AFAreaHeight","native":{"Name":"AFAreaHeight","_shorthand":true},"refusals":[]}],"serial_index":7,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":0},"format":"int16s","format_source":"field"},"groups":{},"name":"AFAreaXPositions","native":{"Format":"int16s[$val{0}]","Name":"AFAreaXPositions"},"refusals":[]}],"serial_index":8,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":0},"format":"int16s","format_source":"field"},"groups":{},"name":"AFAreaYPositions","native":{"Format":"int16s[$val{0}]","Name":"AFAreaYPositions"},"refusals":[]}],"serial_index":9,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"expression":"Image::ExifTool::DecodeBits($val, undef, 16)","kind":"native_expr"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"add":15,"divisor":16,"kind":"floor_div_prior_raw_value","serial_index":0},"format":"int16s","format_source":"field"},"groups":{},"name":"AFPointsInFocus","native":{"Format":"int16s[int(($val{0}+15)/16)]","Name":"AFPointsInFocus","PrintConv":{"expr":"Image::ExifTool::DecodeBits($val, undef, 16)","kind":"expr"}},"refusals":["serial_decode_bits_words"]}],"serial_index":10,"variant":false},{"alternatives":[{"condition":{"compiled":"Cond::And(&Cond::MemberRegex { member: \"Model\", pattern: \"EOS\", ignore_case: false, negate: true }, &Cond::Or(&Cond::MemberTruthy { member: \"AFInfoCount\", negate: true }, &Cond::MemberCmp { member: \"AFInfoCount\", op: CmpOp::Ne, value: 36 }))","kind":"shared_cond","missing_member":"empty_string","source":"\n $$self{Model} !~ /EOS/ and\n (not $$self{AFInfoCount} or $$self{AFInfoCount} != 36)\n "},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"PrimaryAFPoint","native":{"Condition":"\n $$self{Model} !~ /EOS/ and\n (not $$self{AFInfoCount} or $$self{AFInfoCount} != 36)\n ","Name":"PrimaryAFPoint"},"refusals":[]},{"condition":{"compiled":"Cond::MemberRegex { member: \"Model\", pattern: \"EOS\", ignore_case: false, negate: true }","kind":"shared_cond","missing_member":"empty_string","source":"$$self{Model} !~ /EOS/"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":8},"format":"int16u","format_source":"field"},"groups":{},"name":"Canon_AFInfo_0x000b","native":{"Condition":"$$self{Model} !~ /EOS/","Format":"int16u[8]","Name":"Canon_AFInfo_0x000b","Unknown":"1"},"refusals":[]}],"serial_index":11,"variant":true},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"PrimaryAFPoint","native":{"Name":"PrimaryAFPoint","_shorthand":true},"refusals":[]}],"serial_index":12,"variant":false}],"gate_a":{"blocked_by":[["serial_decode_bits_words",1]]},"kind":"native_serial_layout_inventory","module":"Canon","processor":{"condition_missing_member":"native_perl_empty_string","effects_in_order":["serial processor package","temporary unknown option","unknown generation suppression","serial verbose callback","table default format","serial GetTagInfo","dynamic count evaluation","remainder string","field length","bounded read","raw value storage","subdirectory branch","tag reporting","serial cursor advance","unknown option restore","unknown generation cleanup"],"name":"Image::ExifTool::Canon::ProcessSerialData","prior_value_domain":"raw_read_value_before_conversion_or_reporting","source_body_sha256":"ab5cb31e06a991a02f5569f1c008303c0505d28caf7ae2ec044aeede9c9f999c","source_file":"Image/ExifTool/Canon.pm","source_sha256":"d22b110e7b2e1af2d6e59bb97db97a04ffc2bfd1ba7d078d380ae60fbc70bbba"},"runtime_status":"inventory_only_no_reader_or_route","table":"AFInfo","table_facts":{"default_format":"int16u","documentary":{"NOTES":"\n Auto-focus information used by many older Canon models. The values in this\n record are sequential, and some have variable sizes based on the value of\n NumAFPoints (which may be 1,5,7,9,15,45 or 53). The AFArea coordinates are\n given in a system where the image has dimensions given by AFImageWidth and\n AFImageHeight, and 0,0 is the image center. The direction of the Y axis\n depends on the camera model, with positive Y upwards for EOS models, but\n apparently downwards for PowerShot models.\n "},"groups":{"0":"MakerNotes","2":"Camera"},"native_table_sha256":"45e5f32aa56326d5043149994fcd71ea8056d86af70f73855a64d82065eb7b47","unmodeled":{},"variables":{"ID_LABEL":"Sequence"}},"version":1},"module":"Canon","native_alternatives":14,"native_entries":13,"outcome":"descriptor_compiled","table":"AFInfo"},{"descriptor":{"entries":[{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"AFInfoSize","native":{"Name":"AFInfoSize","Unknown":"1"},"refusals":[]}],"serial_index":0,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"native_print_conv","source":{"directives":null,"kind":"enum","map":{"0":"Off (Manual Focus)","1":"AF Point Expansion (surround)","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","2":"Single-point AF","20":"Flexible Zone AF 2","21":"Flexible Zone AF 3","22":"Whole Area AF","4":"Auto","5":"Face Detect AF","6":"Face + Tracking","7":"Zone AF","8":"AF Point Expansion (4 point)","9":"Spot AF"}}},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"AFAreaMode","native":{"Name":"AFAreaMode","PrintConv":{"directives":null,"kind":"enum","map":{"0":"Off (Manual Focus)","1":"AF Point Expansion (surround)","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","2":"Single-point AF","20":"Flexible Zone AF 2","21":"Flexible Zone AF 3","22":"Whole Area AF","4":"Auto","5":"Face Detect AF","6":"Face + Tracking","7":"Zone AF","8":"AF Point Expansion (4 point)","9":"Spot AF"}}},"refusals":["serial_print_conv"]}],"serial_index":1,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"NumAFPoints","native":{"Name":"NumAFPoints","RawConv":{"expr":"$$self{NumAFPoints} = $val","kind":"expr"}},"refusals":["serial_raw_conv"]}],"serial_index":2,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"ValidAFPoints","native":{"Name":"ValidAFPoints","Notes":"number of AF points valid in the following information"},"refusals":[]}],"serial_index":3,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{"2":"Image"},"name":"CanonImageWidth","native":{"Groups":{"2":"Image"},"Name":"CanonImageWidth"},"refusals":[]}],"serial_index":4,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{"2":"Image"},"name":"CanonImageHeight","native":{"Groups":{"2":"Image"},"Name":"CanonImageHeight"},"refusals":[]}],"serial_index":5,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"AFImageWidth","native":{"Name":"AFImageWidth","Notes":"size of image in AF coordinates"},"refusals":[]}],"serial_index":6,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"AFImageHeight","native":{"Name":"AFImageHeight","_shorthand":true},"refusals":[]}],"serial_index":7,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":2},"format":"int16s","format_source":"field"},"groups":{},"name":"AFAreaWidths","native":{"Format":"int16s[$val{2}]","Name":"AFAreaWidths"},"refusals":[]}],"serial_index":8,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":2},"format":"int16s","format_source":"field"},"groups":{},"name":"AFAreaHeights","native":{"Format":"int16s[$val{2}]","Name":"AFAreaHeights"},"refusals":[]}],"serial_index":9,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":2},"format":"int16s","format_source":"field"},"groups":{},"name":"AFAreaXPositions","native":{"Format":"int16s[$val{2}]","Name":"AFAreaXPositions"},"refusals":[]}],"serial_index":10,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":2},"format":"int16s","format_source":"field"},"groups":{},"name":"AFAreaYPositions","native":{"Format":"int16s[$val{2}]","Name":"AFAreaYPositions"},"refusals":[]}],"serial_index":11,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"expression":"Image::ExifTool::DecodeBits($val, undef, 16)","kind":"native_expr"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"add":15,"divisor":16,"kind":"floor_div_prior_raw_value","serial_index":2},"format":"int16s","format_source":"field"},"groups":{},"name":"AFPointsInFocus","native":{"Format":"int16s[int(($val{2}+15)/16)]","Name":"AFPointsInFocus","PrintConv":{"expr":"Image::ExifTool::DecodeBits($val, undef, 16)","kind":"expr"}},"refusals":["serial_decode_bits_words"]}],"serial_index":12,"variant":false},{"alternatives":[{"condition":{"compiled":"Cond::MemberRegex { member: \"Model\", pattern: \"EOS\", ignore_case: false, negate: false }","kind":"shared_cond","missing_member":"empty_string","source":"$$self{Model} =~ /EOS/"},"conversion":{"expression":"Image::ExifTool::DecodeBits($val, undef, 16)","kind":"native_expr"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"add":15,"divisor":16,"kind":"floor_div_prior_raw_value","serial_index":2},"format":"int16s","format_source":"field"},"groups":{},"name":"AFPointsSelected","native":{"Condition":"$$self{Model} =~ /EOS/","Format":"int16s[int(($val{2}+15)/16)]","Name":"AFPointsSelected","PrintConv":{"expr":"Image::ExifTool::DecodeBits($val, undef, 16)","kind":"expr"}},"refusals":["serial_decode_bits_words"]},{"name":"Canon_AFInfo2_0x000d","native":{"Format":"int16s[int(($val{2}+15)/16)+1]","Name":"Canon_AFInfo2_0x000d","Unknown":"1"},"native_refusal":"serial Format count expression is outside the closed prior-value grammar","refusals":["serial_row_shape"]}],"serial_index":13,"variant":true},{"alternatives":[{"condition":{"compiled":"Cond::And(&Cond::MemberRegex { member: \"Model\", pattern: \"EOS\", ignore_case: false, negate: true }, &Cond::MemberTruthy { member: \"AFInfo3\", negate: true })","kind":"shared_cond","missing_member":"empty_string","source":"$$self{Model} !~ /EOS/ and not $$self{AFInfo3}"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"PrimaryAFPoint","native":{"Condition":"$$self{Model} !~ /EOS/ and not $$self{AFInfo3}","Name":"PrimaryAFPoint"},"refusals":[]}],"serial_index":14,"variant":false}],"gate_a":{"blocked_by":[["serial_decode_bits_words",2],["serial_print_conv",1],["serial_raw_conv",1],["serial_row_shape",1]]},"kind":"native_serial_layout_inventory","module":"Canon","processor":{"condition_missing_member":"native_perl_empty_string","effects_in_order":["serial processor package","temporary unknown option","unknown generation suppression","serial verbose callback","table default format","serial GetTagInfo","dynamic count evaluation","remainder string","field length","bounded read","raw value storage","subdirectory branch","tag reporting","serial cursor advance","unknown option restore","unknown generation cleanup"],"name":"Image::ExifTool::Canon::ProcessSerialData","prior_value_domain":"raw_read_value_before_conversion_or_reporting","source_body_sha256":"ab5cb31e06a991a02f5569f1c008303c0505d28caf7ae2ec044aeede9c9f999c","source_file":"Image/ExifTool/Canon.pm","source_sha256":"d22b110e7b2e1af2d6e59bb97db97a04ffc2bfd1ba7d078d380ae60fbc70bbba"},"runtime_status":"inventory_only_no_reader_or_route","table":"AFInfo2","table_facts":{"default_format":"int16u","documentary":{"NOTES":"\n Newer version of the AFInfo record containing much of the same information\n (and coordinate confusion) as the older version. In this record, NumAFPoints\n may be 7, 9, 11, 19, 31, 45 or 61, depending on the camera model.\n "},"groups":{"0":"MakerNotes","2":"Camera"},"native_table_sha256":"4da030bfec2ba6648fbb3d9783ae56ecf293fa440d701ba3dfe63600a1d8c9db","unmodeled":{},"variables":{"ID_LABEL":"Sequence"}},"version":1},"module":"Canon","native_alternatives":16,"native_entries":15,"outcome":"descriptor_compiled","table":"AFInfo2"},{"descriptor":{"entries":[{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"field"},"groups":{},"name":"Channels","native":{"Format":"int16u","Name":"Channels"},"refusals":[]}],"serial_index":0,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":3},"format":"int16u","format_source":"field"},"groups":{},"name":"Unknown","native":{"Format":"int16u[3]","Name":"Unknown","Unknown":"1"},"refusals":[]}],"serial_index":1,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"field"},"groups":{},"name":"BytesPerMinute","native":{"Format":"int16u","Name":"BytesPerMinute"},"refusals":[]}],"serial_index":2,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"field"},"groups":{},"name":"AudioBytes","native":{"Format":"int32u","Name":"AudioBytes"},"refusals":[]}],"serial_index":3,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int8u","format_source":"table_default"},"groups":{},"name":"TitleLen","native":{"Name":"TitleLen","Unknown":"1"},"refusals":[]}],"serial_index":4,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":4},"format":"string","format_source":"field"},"groups":{},"name":"Title","native":{"Format":"string[$val{4}]","Name":"Title"},"refusals":[]}],"serial_index":5,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int8u","format_source":"table_default"},"groups":{},"name":"ArtistLen","native":{"Name":"ArtistLen","Unknown":"1"},"refusals":[]}],"serial_index":6,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":6},"format":"string","format_source":"field"},"groups":{"2":"Author"},"name":"Artist","native":{"Format":"string[$val{6}]","Groups":{"2":"Author"},"Name":"Artist"},"refusals":[]}],"serial_index":7,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int8u","format_source":"table_default"},"groups":{},"name":"CopyrightLen","native":{"Name":"CopyrightLen","Unknown":"1"},"refusals":[]}],"serial_index":8,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":8},"format":"string","format_source":"field"},"groups":{"2":"Author"},"name":"Copyright","native":{"Format":"string[$val{8}]","Groups":{"2":"Author"},"Name":"Copyright"},"refusals":[]}],"serial_index":9,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int8u","format_source":"table_default"},"groups":{},"name":"CommentLen","native":{"Name":"CommentLen","Unknown":"1"},"refusals":[]}],"serial_index":10,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":10},"format":"string","format_source":"field"},"groups":{},"name":"Comment","native":{"Format":"string[$val{10}]","Name":"Comment"},"refusals":[]}],"serial_index":11,"variant":false}],"gate_a":{"blocked_by":[]},"kind":"native_serial_layout_inventory","module":"Real","processor":{"condition_missing_member":"native_perl_empty_string","effects_in_order":["serial processor package","temporary unknown option","unknown generation suppression","serial verbose callback","table default format","serial GetTagInfo","dynamic count evaluation","remainder string","field length","bounded read","raw value storage","subdirectory branch","tag reporting","serial cursor advance","unknown option restore","unknown generation cleanup"],"name":"Image::ExifTool::Canon::ProcessSerialData","prior_value_domain":"raw_read_value_before_conversion_or_reporting","source_body_sha256":"ab5cb31e06a991a02f5569f1c008303c0505d28caf7ae2ec044aeede9c9f999c","source_file":"Image/ExifTool/Canon.pm","source_sha256":"d22b110e7b2e1af2d6e59bb97db97a04ffc2bfd1ba7d078d380ae60fbc70bbba"},"runtime_status":"inventory_only_no_reader_or_route","table":"AudioV3","table_facts":{"default_format":"int8u","documentary":{},"groups":{"1":"Real-RA3","2":"Audio"},"native_table_sha256":"5b947891835e07639d2640349868fa05c9c5b5f55f7e1c835224a42d3a7ab975","unmodeled":{},"variables":{"ID_LABEL":"Sequence"}},"version":1},"module":"Real","native_alternatives":12,"native_entries":12,"outcome":"descriptor_compiled","table":"AudioV3"},{"descriptor":{"entries":[{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":4},"format":"undef","format_source":"field"},"groups":{},"name":"FourCC1","native":{"Format":"undef[4]","Name":"FourCC1","Unknown":"1"},"refusals":[]}],"serial_index":0,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"field"},"groups":{},"name":"AudioFileSize","native":{"Format":"int32u","Name":"AudioFileSize","Unknown":"1"},"refusals":[]}],"serial_index":1,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"Version2","native":{"Name":"Version2","Unknown":"1"},"refusals":[]}],"serial_index":2,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"field"},"groups":{},"name":"HeaderSize","native":{"Format":"int32u","Name":"HeaderSize","Unknown":"1"},"refusals":[]}],"serial_index":3,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"CodecFlavorID","native":{"Name":"CodecFlavorID","Unknown":"1"},"refusals":[]}],"serial_index":4,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"field"},"groups":{},"name":"CodedFrameSize","native":{"Format":"int32u","Name":"CodedFrameSize","Unknown":"1"},"refusals":[]}],"serial_index":5,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"field"},"groups":{},"name":"AudioBytes","native":{"Format":"int32u","Name":"AudioBytes"},"refusals":[]}],"serial_index":6,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"field"},"groups":{},"name":"BytesPerMinute","native":{"Format":"int32u","Name":"BytesPerMinute"},"refusals":[]}],"serial_index":7,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"field"},"groups":{},"name":"Unknown","native":{"Format":"int32u","Name":"Unknown","Unknown":"1"},"refusals":[]}],"serial_index":8,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"SubPacketH","native":{"Name":"SubPacketH","Unknown":"1"},"refusals":[]}],"serial_index":9,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"AudioFrameSize","native":{"Name":"AudioFrameSize","_shorthand":true},"refusals":[]}],"serial_index":10,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"SubPacketSize","native":{"Name":"SubPacketSize","Unknown":"1"},"refusals":[]}],"serial_index":11,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"Unknown","native":{"Name":"Unknown","Unknown":"1"},"refusals":[]}],"serial_index":12,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"SampleRate","native":{"Name":"SampleRate","_shorthand":true},"refusals":[]}],"serial_index":13,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"Unknown","native":{"Name":"Unknown","Unknown":"1"},"refusals":[]}],"serial_index":14,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"BitsPerSample","native":{"Name":"BitsPerSample","_shorthand":true},"refusals":[]}],"serial_index":15,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"Channels","native":{"Name":"Channels","_shorthand":true},"refusals":[]}],"serial_index":16,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int8u","format_source":"field"},"groups":{},"name":"FourCC2Len","native":{"Format":"int8u","Name":"FourCC2Len","Unknown":"1"},"refusals":[]}],"serial_index":17,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":4},"format":"undef","format_source":"field"},"groups":{},"name":"FourCC2","native":{"Format":"undef[4]","Name":"FourCC2","Unknown":"1"},"refusals":[]}],"serial_index":18,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int8u","format_source":"field"},"groups":{},"name":"FourCC3Len","native":{"Format":"int8u","Name":"FourCC3Len","Unknown":"1"},"refusals":[]}],"serial_index":19,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":4},"format":"undef","format_source":"field"},"groups":{},"name":"FourCC3","native":{"Format":"undef[4]","Name":"FourCC3","Unknown":"1"},"refusals":[]}],"serial_index":20,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int8u","format_source":"field"},"groups":{},"name":"Unknown","native":{"Format":"int8u","Name":"Unknown","Unknown":"1"},"refusals":[]}],"serial_index":21,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"Unknown","native":{"Name":"Unknown","Unknown":"1"},"refusals":[]}],"serial_index":22,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int8u","format_source":"field"},"groups":{},"name":"TitleLen","native":{"Format":"int8u","Name":"TitleLen","Unknown":"1"},"refusals":[]}],"serial_index":23,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":23},"format":"string","format_source":"field"},"groups":{},"name":"Title","native":{"Format":"string[$val{23}]","Name":"Title"},"refusals":[]}],"serial_index":24,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int8u","format_source":"field"},"groups":{},"name":"ArtistLen","native":{"Format":"int8u","Name":"ArtistLen","Unknown":"1"},"refusals":[]}],"serial_index":25,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":25},"format":"string","format_source":"field"},"groups":{"2":"Author"},"name":"Artist","native":{"Format":"string[$val{25}]","Groups":{"2":"Author"},"Name":"Artist"},"refusals":[]}],"serial_index":26,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int8u","format_source":"field"},"groups":{},"name":"CopyrightLen","native":{"Format":"int8u","Name":"CopyrightLen","Unknown":"1"},"refusals":[]}],"serial_index":27,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":27},"format":"string","format_source":"field"},"groups":{"2":"Author"},"name":"Copyright","native":{"Format":"string[$val{27}]","Groups":{"2":"Author"},"Name":"Copyright"},"refusals":[]}],"serial_index":28,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int8u","format_source":"field"},"groups":{},"name":"CommentLen","native":{"Format":"int8u","Name":"CommentLen","Unknown":"1"},"refusals":[]}],"serial_index":29,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":29},"format":"string","format_source":"field"},"groups":{},"name":"Comment","native":{"Format":"string[$val{29}]","Name":"Comment"},"refusals":[]}],"serial_index":30,"variant":false}],"gate_a":{"blocked_by":[]},"kind":"native_serial_layout_inventory","module":"Real","processor":{"condition_missing_member":"native_perl_empty_string","effects_in_order":["serial processor package","temporary unknown option","unknown generation suppression","serial verbose callback","table default format","serial GetTagInfo","dynamic count evaluation","remainder string","field length","bounded read","raw value storage","subdirectory branch","tag reporting","serial cursor advance","unknown option restore","unknown generation cleanup"],"name":"Image::ExifTool::Canon::ProcessSerialData","prior_value_domain":"raw_read_value_before_conversion_or_reporting","source_body_sha256":"ab5cb31e06a991a02f5569f1c008303c0505d28caf7ae2ec044aeede9c9f999c","source_file":"Image/ExifTool/Canon.pm","source_sha256":"d22b110e7b2e1af2d6e59bb97db97a04ffc2bfd1ba7d078d380ae60fbc70bbba"},"runtime_status":"inventory_only_no_reader_or_route","table":"AudioV4","table_facts":{"default_format":"int16u","documentary":{},"groups":{"1":"Real-RA4","2":"Audio"},"native_table_sha256":"9fa6f8407b6678ed007722829d5e2974b9d5034fbd6b61f22fbebeaf6d0d1065","unmodeled":{},"variables":{"ID_LABEL":"Sequence"}},"version":1},"module":"Real","native_alternatives":31,"native_entries":31,"outcome":"descriptor_compiled","table":"AudioV4"},{"descriptor":{"entries":[{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":4},"format":"undef","format_source":"field"},"groups":{},"name":"FourCC1","native":{"Format":"undef[4]","Name":"FourCC1","Unknown":"1"},"refusals":[]}],"serial_index":0,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"field"},"groups":{},"name":"AudioFileSize","native":{"Format":"int32u","Name":"AudioFileSize","Unknown":"1"},"refusals":[]}],"serial_index":1,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"Version2","native":{"Name":"Version2","Unknown":"1"},"refusals":[]}],"serial_index":2,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"field"},"groups":{},"name":"HeaderSize","native":{"Format":"int32u","Name":"HeaderSize","Unknown":"1"},"refusals":[]}],"serial_index":3,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"CodecFlavorID","native":{"Name":"CodecFlavorID","Unknown":"1"},"refusals":[]}],"serial_index":4,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"field"},"groups":{},"name":"CodedFrameSize","native":{"Format":"int32u","Name":"CodedFrameSize","Unknown":"1"},"refusals":[]}],"serial_index":5,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"field"},"groups":{},"name":"AudioBytes","native":{"Format":"int32u","Name":"AudioBytes"},"refusals":[]}],"serial_index":6,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"field"},"groups":{},"name":"BytesPerMinute","native":{"Format":"int32u","Name":"BytesPerMinute"},"refusals":[]}],"serial_index":7,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"field"},"groups":{},"name":"Unknown","native":{"Format":"int32u","Name":"Unknown","Unknown":"1"},"refusals":[]}],"serial_index":8,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"SubPacketH","native":{"Name":"SubPacketH","Unknown":"1"},"refusals":[]}],"serial_index":9,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"FrameSize","native":{"Name":"FrameSize","Unknown":"1"},"refusals":[]}],"serial_index":10,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"SubPacketSize","native":{"Name":"SubPacketSize","Unknown":"1"},"refusals":[]}],"serial_index":11,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"SampleRate","native":{"Name":"SampleRate","_shorthand":true},"refusals":[]}],"serial_index":12,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"SampleRate2","native":{"Name":"SampleRate2","Unknown":"1"},"refusals":[]}],"serial_index":13,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"field"},"groups":{},"name":"BitsPerSample","native":{"Format":"int32u","Name":"BitsPerSample"},"refusals":[]}],"serial_index":14,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"Channels","native":{"Name":"Channels","_shorthand":true},"refusals":[]}],"serial_index":15,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"field"},"groups":{},"name":"Genr","native":{"Format":"int32u","Name":"Genr","Unknown":"1"},"refusals":[]}],"serial_index":16,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":4},"format":"undef","format_source":"field"},"groups":{},"name":"FourCC3","native":{"Format":"undef[4]","Name":"FourCC3","Unknown":"1"},"refusals":[]}],"serial_index":17,"variant":false}],"gate_a":{"blocked_by":[]},"kind":"native_serial_layout_inventory","module":"Real","processor":{"condition_missing_member":"native_perl_empty_string","effects_in_order":["serial processor package","temporary unknown option","unknown generation suppression","serial verbose callback","table default format","serial GetTagInfo","dynamic count evaluation","remainder string","field length","bounded read","raw value storage","subdirectory branch","tag reporting","serial cursor advance","unknown option restore","unknown generation cleanup"],"name":"Image::ExifTool::Canon::ProcessSerialData","prior_value_domain":"raw_read_value_before_conversion_or_reporting","source_body_sha256":"ab5cb31e06a991a02f5569f1c008303c0505d28caf7ae2ec044aeede9c9f999c","source_file":"Image/ExifTool/Canon.pm","source_sha256":"d22b110e7b2e1af2d6e59bb97db97a04ffc2bfd1ba7d078d380ae60fbc70bbba"},"runtime_status":"inventory_only_no_reader_or_route","table":"AudioV5","table_facts":{"default_format":"int16u","documentary":{},"groups":{"1":"Real-RA5","2":"Audio"},"native_table_sha256":"41ab70aabcb492d0d251e4a4f32d1b1e6d7d008e7a7d791bf0fc9bd6294d41ce","unmodeled":{},"variables":{"ID_LABEL":"Sequence"}},"version":1},"module":"Real","native_alternatives":18,"native_entries":18,"outcome":"descriptor_compiled","table":"AudioV5"},{"descriptor":{"entries":[{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"TitleLen","native":{"Name":"TitleLen","Unknown":"1"},"refusals":[]}],"serial_index":0,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":0},"format":"string","format_source":"field"},"groups":{},"name":"Title","native":{"Format":"string[$val{0}]","Name":"Title"},"refusals":[]}],"serial_index":1,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"AuthorLen","native":{"Name":"AuthorLen","Unknown":"1"},"refusals":[]}],"serial_index":2,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":2},"format":"string","format_source":"field"},"groups":{"2":"Author"},"name":"Author","native":{"Format":"string[$val{2}]","Groups":{"2":"Author"},"Name":"Author"},"refusals":[]}],"serial_index":3,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"CopyrightLen","native":{"Name":"CopyrightLen","Unknown":"1"},"refusals":[]}],"serial_index":4,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":4},"format":"string","format_source":"field"},"groups":{"2":"Author"},"name":"Copyright","native":{"Format":"string[$val{4}]","Groups":{"2":"Author"},"Name":"Copyright"},"refusals":[]}],"serial_index":5,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"table_default"},"groups":{},"name":"CommentLen","native":{"Name":"CommentLen","Unknown":"1"},"refusals":[]}],"serial_index":6,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":6},"format":"string","format_source":"field"},"groups":{},"name":"Comment","native":{"Format":"string[$val{6}]","Name":"Comment"},"refusals":[]}],"serial_index":7,"variant":false}],"gate_a":{"blocked_by":[]},"kind":"native_serial_layout_inventory","module":"Real","processor":{"condition_missing_member":"native_perl_empty_string","effects_in_order":["serial processor package","temporary unknown option","unknown generation suppression","serial verbose callback","table default format","serial GetTagInfo","dynamic count evaluation","remainder string","field length","bounded read","raw value storage","subdirectory branch","tag reporting","serial cursor advance","unknown option restore","unknown generation cleanup"],"name":"Image::ExifTool::Canon::ProcessSerialData","prior_value_domain":"raw_read_value_before_conversion_or_reporting","source_body_sha256":"ab5cb31e06a991a02f5569f1c008303c0505d28caf7ae2ec044aeede9c9f999c","source_file":"Image/ExifTool/Canon.pm","source_sha256":"d22b110e7b2e1af2d6e59bb97db97a04ffc2bfd1ba7d078d380ae60fbc70bbba"},"runtime_status":"inventory_only_no_reader_or_route","table":"ContentDescr","table_facts":{"default_format":"int16u","documentary":{},"groups":{"1":"Real-CONT","2":"Video"},"native_table_sha256":"919d184e2424472d43929633f512c18440494f7058142a82d5e5c0de02667c1c","unmodeled":{},"variables":{"ID_LABEL":"Sequence"}},"version":1},"module":"Real","native_alternatives":8,"native_entries":8,"outcome":"descriptor_compiled","table":"ContentDescr"},{"descriptor":{"entries":[{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"field"},"groups":{},"name":"StreamNumber","native":{"Format":"int16u","Name":"StreamNumber"},"refusals":[]}],"serial_index":0,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"expression":"ConvertBitrate($val)","kind":"native_expr"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"table_default"},"groups":{},"name":"StreamMaxBitrate","native":{"Name":"StreamMaxBitrate","PrintConv":{"expr":"ConvertBitrate($val)","kind":"expr"}},"refusals":["serial_print_conv_expr"]}],"serial_index":1,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"expression":"ConvertBitrate($val)","kind":"native_expr"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"table_default"},"groups":{},"name":"StreamAvgBitrate","native":{"Name":"StreamAvgBitrate","PrintConv":{"expr":"ConvertBitrate($val)","kind":"expr"}},"refusals":["serial_print_conv_expr"]}],"serial_index":2,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"table_default"},"groups":{},"name":"StreamMaxPacketSize","native":{"Name":"StreamMaxPacketSize"},"refusals":[]}],"serial_index":3,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"table_default"},"groups":{},"name":"StreamAvgPacketSize","native":{"Name":"StreamAvgPacketSize"},"refusals":[]}],"serial_index":4,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"table_default"},"groups":{},"name":"StreamStartTime","native":{"Name":"StreamStartTime"},"refusals":[]}],"serial_index":5,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"expression":"ConvertDuration($val)","kind":"native_expr"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"table_default"},"groups":{},"name":"StreamPreroll","native":{"Name":"StreamPreroll","PrintConv":{"expr":"ConvertDuration($val)","kind":"expr"},"ValueConv":{"expr":"$val / 1000","kind":"expr"}},"refusals":["serial_print_conv_expr","serial_value_conv"]}],"serial_index":6,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"expression":"ConvertDuration($val)","kind":"native_expr"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"table_default"},"groups":{},"name":"StreamDuration","native":{"Name":"StreamDuration","PrintConv":{"expr":"ConvertDuration($val)","kind":"expr"},"ValueConv":{"expr":"$val / 1000","kind":"expr"}},"refusals":["serial_print_conv_expr","serial_value_conv"]}],"serial_index":7,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int8u","format_source":"field"},"groups":{},"name":"StreamNameLen","native":{"Format":"int8u","Name":"StreamNameLen","Unknown":"1"},"refusals":[]}],"serial_index":8,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":8},"format":"string","format_source":"field"},"groups":{},"name":"StreamName","native":{"Format":"string[$val{8}]","Name":"StreamName"},"refusals":[]}],"serial_index":9,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int8u","format_source":"field"},"groups":{},"name":"StreamMimeLen","native":{"Format":"int8u","Name":"StreamMimeLen","Unknown":"1"},"refusals":[]}],"serial_index":10,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"prior_raw_value","serial_index":10},"format":"string","format_source":"field"},"groups":{},"name":"StreamMimeType","native":{"Format":"string[$val{10}]","Name":"StreamMimeType","RawConv":{"expr":"$self->{RealStreamMime} = $val","kind":"expr"}},"refusals":["serial_raw_conv"]}],"serial_index":11,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"table_default"},"groups":{},"name":"FileInfoLen","native":{"Name":"FileInfoLen","Unknown":"1"},"refusals":[]}],"serial_index":12,"variant":false},{"alternatives":[{"condition":{"compiled":"Cond::MemberStrEq { member: \"RealStreamMime\", value: \"logical-fileinfo\", negate: false }","kind":"shared_cond","missing_member":"shared_default","source":"$self->{RealStreamMime} eq \"logical-fileinfo\""},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"table_default"},"groups":{},"name":"FileInfoLen2","native":{"Condition":"$self->{RealStreamMime} eq \"logical-fileinfo\"","Name":"FileInfoLen2","Unknown":"1"},"refusals":[]}],"serial_index":13,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"field"},"groups":{},"name":"FileInfoVersion","native":{"Format":"int16u","Name":"FileInfoVersion"},"refusals":[]}],"serial_index":14,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"field"},"groups":{},"name":"PhysicalStreams","native":{"Format":"int16u","Name":"PhysicalStreams","Unknown":"1"},"refusals":[]}],"serial_index":15,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"prior_raw_value","serial_index":15},"format":"int16u","format_source":"field"},"groups":{},"name":"PhysicalStreamNumbers","native":{"Format":"int16u[$val{15}]","Name":"PhysicalStreamNumbers","Unknown":"1"},"refusals":[]}],"serial_index":16,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"prior_raw_value","serial_index":15},"format":"int32u","format_source":"field"},"groups":{},"name":"DataOffsets","native":{"Format":"int32u[$val{15}]","Name":"DataOffsets","Unknown":"1"},"refusals":[]}],"serial_index":17,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"field"},"groups":{},"name":"NumRules","native":{"Format":"int16u","Name":"NumRules","Unknown":"1"},"refusals":[]}],"serial_index":18,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"prior_raw_value","serial_index":18},"format":"int16u","format_source":"field"},"groups":{},"name":"PhysicalStreamNumberMap","native":{"Format":"int16u[$val{18}]","Name":"PhysicalStreamNumberMap","Unknown":"1"},"refusals":[]}],"serial_index":19,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"field"},"groups":{},"name":"NumProperties","native":{"Format":"int16u","Name":"NumProperties","Unknown":"1"},"refusals":[]}],"serial_index":20,"variant":false},{"alternatives":[{"name":"FileInfoProperties","native":{"Format":"undef[$val{13}-$val{15}*6-$val{18}*2-12]","Name":"FileInfoProperties","SubDirectory":{"TagTable":"Image::ExifTool::Real::FileInfo"}},"native_refusal":"serial Format count expression is outside the closed prior-value grammar","refusals":["serial_row_shape"]}],"serial_index":21,"variant":false}],"gate_a":{"blocked_by":[["serial_print_conv_expr",4],["serial_raw_conv",1],["serial_row_shape",1],["serial_table_priority",1],["serial_value_conv",2]]},"kind":"native_serial_layout_inventory","module":"Real","processor":{"condition_missing_member":"native_perl_empty_string","effects_in_order":["serial processor package","temporary unknown option","unknown generation suppression","serial verbose callback","table default format","serial GetTagInfo","dynamic count evaluation","remainder string","field length","bounded read","raw value storage","subdirectory branch","tag reporting","serial cursor advance","unknown option restore","unknown generation cleanup"],"name":"Image::ExifTool::Canon::ProcessSerialData","prior_value_domain":"raw_read_value_before_conversion_or_reporting","source_body_sha256":"ab5cb31e06a991a02f5569f1c008303c0505d28caf7ae2ec044aeede9c9f999c","source_file":"Image/ExifTool/Canon.pm","source_sha256":"d22b110e7b2e1af2d6e59bb97db97a04ffc2bfd1ba7d078d380ae60fbc70bbba"},"runtime_status":"inventory_only_no_reader_or_route","table":"MediaProps","table_facts":{"default_format":"int32u","documentary":{},"groups":{"1":"Real-MDPR","2":"Video"},"native_table_sha256":"7f027a5be2dd62b4d6b9693f05782ad5dabff5e9b6bcaf47dc2db6d9cfd6923b","unmodeled":{"PRIORITY":"0"},"variables":{"ID_LABEL":"Sequence"}},"version":1},"module":"Real","native_alternatives":22,"native_entries":22,"outcome":"descriptor_compiled","table":"MediaProps"},{"descriptor":{"entries":[{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"expression":"ConvertBitrate($val)","kind":"native_expr"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"table_default"},"groups":{},"name":"MaxBitrate","native":{"Name":"MaxBitrate","PrintConv":{"expr":"ConvertBitrate($val)","kind":"expr"}},"refusals":["serial_print_conv_expr"]}],"serial_index":0,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"expression":"ConvertBitrate($val)","kind":"native_expr"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"table_default"},"groups":{},"name":"AvgBitrate","native":{"Name":"AvgBitrate","PrintConv":{"expr":"ConvertBitrate($val)","kind":"expr"}},"refusals":["serial_print_conv_expr"]}],"serial_index":1,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"table_default"},"groups":{},"name":"MaxPacketSize","native":{"Name":"MaxPacketSize","_shorthand":true},"refusals":[]}],"serial_index":2,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"table_default"},"groups":{},"name":"AvgPacketSize","native":{"Name":"AvgPacketSize","_shorthand":true},"refusals":[]}],"serial_index":3,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"table_default"},"groups":{},"name":"NumPackets","native":{"Name":"NumPackets","_shorthand":true},"refusals":[]}],"serial_index":4,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"expression":"ConvertDuration($val)","kind":"native_expr"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"table_default"},"groups":{},"name":"Duration","native":{"Name":"Duration","PrintConv":{"expr":"ConvertDuration($val)","kind":"expr"},"ValueConv":{"expr":"$val / 1000","kind":"expr"}},"refusals":["serial_print_conv_expr","serial_value_conv"]}],"serial_index":5,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"expression":"ConvertDuration($val)","kind":"native_expr"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"table_default"},"groups":{},"name":"Preroll","native":{"Name":"Preroll","PrintConv":{"expr":"ConvertDuration($val)","kind":"expr"},"ValueConv":{"expr":"$val / 1000","kind":"expr"}},"refusals":["serial_print_conv_expr","serial_value_conv"]}],"serial_index":6,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"table_default"},"groups":{},"name":"IndexOffset","native":{"Name":"IndexOffset","Unknown":"1"},"refusals":[]}],"serial_index":7,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":true},"format":{"count":{"kind":"fixed","value":1},"format":"int32u","format_source":"table_default"},"groups":{},"name":"DataOffset","native":{"Name":"DataOffset","Unknown":"1"},"refusals":[]}],"serial_index":8,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"none"},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"field"},"groups":{},"name":"NumStreams","native":{"Format":"int16u","Name":"NumStreams"},"refusals":[]}],"serial_index":9,"variant":false},{"alternatives":[{"condition":{"kind":"always","missing_member":"not_applicable"},"conversion":{"kind":"native_print_conv","source":{"directives":{"BITMASK":{"0":"Allow Recording","1":"Perfect Play","2":"Live","3":"Allow Download"}},"kind":"enum_partial","map":{}}},"flags":{"binary":false,"list":false,"unknown":false},"format":{"count":{"kind":"fixed","value":1},"format":"int16u","format_source":"field"},"groups":{},"name":"Flags","native":{"Format":"int16u","Name":"Flags","PrintConv":{"directives":{"BITMASK":{"0":"Allow Recording","1":"Perfect Play","2":"Live","3":"Allow Download"}},"kind":"enum_partial","map":{}}},"refusals":["serial_print_conv"]}],"serial_index":10,"variant":false}],"gate_a":{"blocked_by":[["serial_print_conv",1],["serial_print_conv_expr",4],["serial_value_conv",2]]},"kind":"native_serial_layout_inventory","module":"Real","processor":{"condition_missing_member":"native_perl_empty_string","effects_in_order":["serial processor package","temporary unknown option","unknown generation suppression","serial verbose callback","table default format","serial GetTagInfo","dynamic count evaluation","remainder string","field length","bounded read","raw value storage","subdirectory branch","tag reporting","serial cursor advance","unknown option restore","unknown generation cleanup"],"name":"Image::ExifTool::Canon::ProcessSerialData","prior_value_domain":"raw_read_value_before_conversion_or_reporting","source_body_sha256":"ab5cb31e06a991a02f5569f1c008303c0505d28caf7ae2ec044aeede9c9f999c","source_file":"Image/ExifTool/Canon.pm","source_sha256":"d22b110e7b2e1af2d6e59bb97db97a04ffc2bfd1ba7d078d380ae60fbc70bbba"},"runtime_status":"inventory_only_no_reader_or_route","table":"Properties","table_facts":{"default_format":"int32u","documentary":{},"groups":{"1":"Real-PROP","2":"Video"},"native_table_sha256":"b0fc620c04edba01c0667497be39b4a5ff3753c9850e64a8ee7da1ef5fc6d003","unmodeled":{},"variables":{"ID_LABEL":"Sequence"}},"version":1},"module":"Real","native_alternatives":11,"native_entries":11,"outcome":"descriptor_compiled","table":"Properties"}],"version":1} diff --git a/docs/reference/serial-layout-inventory.md b/docs/reference/serial-layout-inventory.md index 623dfb494..06d2d9e18 100644 --- a/docs/reference/serial-layout-inventory.md +++ b/docs/reference/serial-layout-inventory.md @@ -1,15 +1,14 @@ # Staged native serial-layout inventory `tools/exiftool-tables/serial_directory.py` compiles a captured native -`ProcessSerialData` table into a versioned JSON descriptor. It is a source -inventory only: it has no Rust literal, reader, enablement entry or carrier -route. +`ProcessSerialData` table into a versioned JSON descriptor. It is a source inventory and source-derived literal emitter only: it has no +reader, enablement entry or carrier route. -It can also inventory the complete source-selected population rather than a -hand-picked table: +It can also render inactive Rust facts from the complete source-selected +population rather than a hand-picked table: ```sh -python3 tools/exiftool-tables/serial_directory.py "$DUMP" --output "$OUT" +python3 tools/exiftool-tables/serial_directory.py "$DUMP" --output "$OUT" --rust-output "$SERIAL_OUT" ``` `$DUMP` is a recorded `dump_tables.pl` JSON capture from the pinned native @@ -52,11 +51,14 @@ grammar and continue to flow from the captured source facts. The committed 13.59 recorded-input report `serial-layout-inventory-processserialdata.json` contains eight selected tables: 130 native entries and 132 native alternatives. Eight tables produce descriptor -records, 115 alternatives clear the descriptor's row gate, 17 retain named -refusals, four tables have no row-level Gate A blocker, and no selected table is -empty. `Real::MediaProps` also retains its native `PRIORITY => 0` as the named +records, 115 alternatives clear the descriptor's source row gate, 17 retain named +source refusals, four tables have no row-level Gate A blocker, and no selected +table is empty. The emitter adds a separate runtime-readiness refusal for the +four AFInfo/AFInfo2 alternatives whose native missing regex member is an empty +string; current shared `Cond` semantics do not represent that policy. `Real::MediaProps` also retains its native `PRIORITY => 0` as the named table-level blocker `serial_table_priority`; this checkpoint does not discard collision/reporting policy. `Real::AudioV3` is the non-AFInfo control: it uses the same resolved -processor and has 12 source rows with no row-level refusal. This describes -source facts; it does not prove a Rust reader, parent dispatch, or carrier -route. +processor and has 12 source rows with no row-level refusal. The generated literal has `ALL_SERIAL_TABLES` plus sorted native-row/table +omission sidecars. Table Gate A blocks any partial table from reader execution. +This describes source facts; it does not prove a Rust reader, parent dispatch, +or carrier route. diff --git a/docs/reference/serial-runtime-checkpoint.md b/docs/reference/serial-runtime-checkpoint.md new file mode 100644 index 000000000..aa8b7b70d --- /dev/null +++ b/docs/reference/serial-runtime-checkpoint.md @@ -0,0 +1,111 @@ +# Shared serial runtime checkpoint + +This checkpoint makes the native serial layouts executable by one shared +reader. It has no production caller. The next delivery replaces Real AudioV4's +manual field sequence after carrier and output checks pass. + +## Source and publication + +The base is merged PR #756 (`eb700430`), pinned to ExifTool 13.59. Runtime, +emitter and independent verifier are integrated at `9693ef9e` on +`codex/shared-serial-integration-20260913`. The earlier compiled checkpoint +`ff383b8e` is published. Its Rust source is unchanged by the later verifier and +pipeline additions. The final checkpoint still needs hosted acceptance. + +Native inventory selects all tables whose processor is `ProcessSerialData` +before examining generated output. Eight tables contain 130 entries and 132 +alternatives. The source descriptor has 115 clear alternatives and 17 named +refusals. Rust emission adds unsupported-format and missing-member-condition +refusals, producing 106 alternatives and 26 row omission records. No table or +alternative disappears from the inventory. + +| Table | Emitted alternatives | Definition gate | +| --- | ---: | --- | +| Canon AFInfo | 9 | Blocked | +| Canon AFInfo2 | 6 | Blocked | +| Real AudioV3 | 12 | Clear | +| Real AudioV4 | 31 | Clear | +| Real AudioV5 | 18 | Clear | +| Real ContentDescr | 8 | Clear | +| Real MediaProps | 16 | Blocked, including native priority policy | +| Real Properties | 6 | Blocked | + +A clear definition gate does not enable a parser or prove complete execution. +Native/Rust execution currently covers AudioV3 and AudioV4. Caller enablement +is false by default. + +## Independent checks + +`verify_serial_directory.py` reads the emitted Rust independently of the +compiler and compares it with a fresh captured native population. It checks +source bindings, group defaults, fields, counts, conditions, flags, gate +reasons and omission records. The recorded result is eight tables / 132 +alternatives / 106 emitted / 26 omitted, with zero mismatches. + +Review found and closed two false-success cases: claiming every supported +table was descriptor-refused, and appending an omission for a nonexistent +table. Both now fail. Removed rows, changed counts/groups/source hashes, +stale native source and ambiguous single-alternative variant identity also +fail. Nested module group defaults are checked against actual native +`GetTagTable` behavior. This verifier proves source facts; it does not replace +runtime comparison. + +The Rust reader's 13 unit tests pass. The explicit +`real_audio_serial_tables_replay_pinned_native_callbacks` test runs the actual +generated AudioV3/V4 tables against `probe_serial_processor.pl`: ten cases +cover both byte orders, Unknown policy, zero/NUL/truncated strings and a +nonzero bounded directory offset. It passes on both threaded and non-threaded +Perl 5.38.2. It compares callback scalar/string values and native source group +facts, not final metadata grouping or production activation. CI invokes this +ignored test explicitly and requires one passed test, preventing an empty +selector from passing. + +The combined focused Python suite passes all 64 tests, zero failures and zero +skips, in 65.023 seconds. The full `cargo clippy --all-features -- -D warnings` +check passes in 10.125 seconds, and `cargo fmt --all -- --check` passes. These +local checks precede the required full hosted acceptance run. + +The first combined compilation found an invalid byte-order enum spelling in +a test; review also caught a fixture using ASCII 52 instead of length 4. Both +were corrected before the passing run. These were test setup errors, not +claimed output improvements. + +## Reproduction and build cost + +`regen-all.sh` now owns 32 declared artifacts: 10 primary outputs and 22 +secondary outputs. `serial_tables.rs` is generated from the same fresh dump, +formatted, and checked by the independent verifier. The manifest, write guard, +`just verify-tables`, CI and failure-propagation tests include it. + +The official two-tier run at `9693ef9e` passes in 190.956 seconds. All generated +Rust remains unchanged. Only the expression ledger's invocation paths change +when absolute local tool locations are supplied. Re-running that producer with +the portable library path and interpreter command takes 19.028 seconds and +reproduces the committed ledger exactly. No generated artifact was edited by +hand. The initial 22.583-second attempt stopped on the untracked handoff's +dirty-tree guard; the successful run explicitly records the permitted override. +All 607 expressions pass 16,789 applicable native comparisons; 14 probe inputs +are inapplicable. + +The first corrected reader build/test takes 74.437 seconds; the native test +build and run takes 48.276 seconds. A second native replay reuses the exact +compiled binary and finishes in 0.837 seconds. Terra workers perform source +work in separate checkouts, while the coordinator owns the shared build cache. +No i7 job or daemon change is part of this checkpoint. + +## What remains + +- Complete final hosted checks and merge this inactive runtime checkpoint. +- Validate the separately published AudioV4 carrier draft (`118afac7`) with a + fresh candidate/control, real and constructed files, source-change flow and + the full corpus, then remove its manual 31-slot sequence. +- Preserve the carrier's known output-model gaps: native group 0/group 2 are + not stored by the current occurrence API; header-only native warning output + is absent; AudioV3/V5 are not activated. Do not call these complete parity. +- Prove or explicitly refuse zero-count numeric prior reuse, verbose output + and other unexercised conversion/binary behavior before broader enablement. +- Resolve the remaining Canon child processor and four parent omissions, + prove both Canon carriers, then retire their duplicate readers. + +No new project autogenerated percentage or manual-reader retirement is claimed +by this checkpoint. diff --git a/justfile b/justfile index b95ac92ad..df640f54c 100644 --- a/justfile +++ b/justfile @@ -1312,6 +1312,11 @@ verify-tables version="": --keyed-generated src/exiftool_tables/keyed_tables.rs \ --word-processor Image::ExifTool::CanonCustom::ProcessCanonCustom \ --native-inventory --native-inventory-table Sony:Tag202a + # Serial completeness is checked against a fresh native source population. + SERIAL_DUMP="$CACHE/serial-verify-$VERSION.json" + "${EXIFTOOL_PERL:-perl}" tools/exiftool-tables/dump_tables.pl "$LIB" > "$SERIAL_DUMP" + python3 tools/exiftool-tables/verify_serial_directory.py \ + src/exiftool_tables/serial_tables.rs "$SERIAL_DUMP" # Step 27's structure check above proves an edge was transcribed. This # live-Perl oracle proves its generated Start/Base arithmetic evaluates # identically (ExifTool.pm:10118-10137), using this exact pinned tree. diff --git a/src/exiftool_tables/mod.rs b/src/exiftool_tables/mod.rs index b36073061..b1cf1924a 100644 --- a/src/exiftool_tables/mod.rs +++ b/src/exiftool_tables/mod.rs @@ -49,6 +49,9 @@ pub mod keyed_engine; pub mod keyed_schema; pub mod keyed_tables; pub mod runtime; +pub mod serial_engine; +pub mod serial_schema; +pub mod serial_tables; pub mod subdir; pub mod validation; @@ -87,6 +90,16 @@ pub use runtime::{ decode_binary_table_variants, decode_bits, fractional_census, to_exiftool_value, to_tag_value, unknown_fallback, }; +pub use serial_engine::{ + SerialDir, SerialEmissionSink, SerialWalkResult, process_serial_directory, +}; +pub use serial_schema::{ + OmittedSerialNativeRow, OmittedSerialNativeTable, SerialCount, SerialEntry, SerialFormat, + SerialProcessorFacts, SerialTable, SerialTag, +}; +pub use serial_tables::{ + ALL_SERIAL_TABLES, OMITTED_SERIAL_NATIVE_ROWS, OMITTED_SERIAL_NATIVE_TABLES, +}; pub use subdir::{BaseExpr, ByteOrderRule, Start, StartExpr, SubdirEdge}; pub use validation::{SizeExpectation, U16SizeCheck}; @@ -111,6 +124,16 @@ pub fn find_keyed_table(module: &str, table: &str) -> Option<&'static KeyedDirec .find(|t| t.module == module && t.table == table) } +/// Look up source-generated serial facts. A table in this inventory remains +/// inactive until a caller explicitly enables it through [`SerialEmissionSink`]. +#[must_use] +pub fn find_serial_table(module: &str, table: &str) -> Option<&'static SerialTable> { + ALL_SERIAL_TABLES + .iter() + .copied() + .find(|t| t.module == module && t.table == table) +} + /// Look up a generated IFD-style (`Exif::ProcessExif`) table by ExifTool /// module and table name, e.g. `("Olympus", "Main")` -- [`find_table`]'s /// sibling for [`ALL_IFD_TABLES`]. The two name spaces are disjoint by diff --git a/src/exiftool_tables/serial_engine.rs b/src/exiftool_tables/serial_engine.rs new file mode 100644 index 000000000..e3d814476 --- /dev/null +++ b/src/exiftool_tables/serial_engine.rs @@ -0,0 +1,967 @@ +//! Reader for source-described `ProcessSerialData` layouts. +//! +//! This is intentionally a caller-less shared primitive. The caller supplies +//! a bounded directory and must opt in through [`SerialEmissionSink`]. The +//! generated schema supplies all source-specific slot, format, count, +//! condition, conversion and group data. + +use std::collections::HashMap; + +use crate::core::TagValue; +use crate::io::ByteOrder; + +use super::IfdFlags; +use super::cond::Ctx; +use super::engine::{self, Emitted}; +use super::runtime::{self, DecodedValue}; +use super::serial_schema::{SerialCount, SerialEntry, SerialTable, SerialTag}; + +/// The bounded `dirInfo` values ProcessSerialData receives from its carrier. +#[derive(Clone, Copy, Debug)] +pub struct SerialDir<'a> { + pub data: &'a [u8], + pub dir_start: usize, + pub dir_len: usize, + pub base: i64, + pub data_pos: i64, + pub byte_order: ByteOrder, +} + +/// A caller's projection target for a serial directory. +/// +/// Gate B defaults to false. Adding generated serial tables therefore cannot +/// activate a production parser without a carrier explicitly opting in. +pub trait SerialEmissionSink { + fn emit(&mut self, row: Emitted); + + fn serial_enabled(&self, _table: &'static SerialTable) -> bool { + false + } + + /// ProcessSerialData temporarily enables Unknown for `GetTagInfo` to + /// retain cursor alignment, but it calls FoundTag for an Unknown row only + /// if the caller originally requested Unknown output. + fn unknown_enabled(&self) -> bool { + false + } +} + +/// Counted outcomes of one serial walk. Every non-output path is explicit so +/// a future carrier cannot mistake a withheld row for native success. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct SerialWalkResult { + pub entries_seen: usize, + pub emitted: usize, + pub gate_a_blocked: usize, + pub gate_b_blocked: usize, + pub no_matching_alternative: usize, + pub malformed_layout: usize, + pub unreadable_value: usize, + pub unavailable_prior_raw: usize, + pub unavailable_condition_context: usize, + pub omitted: usize, + pub tainted: bool, +} + +enum Selection { + Selected { + tag: &'static SerialTag, + condition_resolved: bool, + }, + NoMatch, + UnavailableContext, + RefusedCondition, +} + +/// Walk a source-described serial table in native key and cursor order. +/// +/// `GetTagInfo` selection happens before a selected row's count and bytes are +/// computed. Once ReadValue succeeds its raw value is retained under this +/// index before reporting, exactly so a later count expression observes raw +/// native data rather than rendered output. +#[must_use] +pub fn process_serial_directory( + table: &'static SerialTable, + dir: SerialDir<'_>, + ctx: &mut Ctx, + sink: &mut dyn SerialEmissionSink, +) -> SerialWalkResult { + let mut result = SerialWalkResult::default(); + if !table.gate_a.passes() { + result.gate_a_blocked = 1; + return result; + } + if !sink.serial_enabled(table) { + result.gate_b_blocked = 1; + return result; + } + + let mut prior_raw = HashMap::::new(); + let mut pos = 0usize; + for (expected_index, entry) in table.entries.iter().enumerate() { + if entry.serial_index != expected_index { + result.malformed_layout += 1; + result.tainted = true; + break; + } + if pos > dir.dir_len { + break; + } + result.entries_seen += 1; + let selected = select(entry, ctx); + let (tag, condition_resolved) = match selected { + Selection::Selected { + tag, + condition_resolved, + } => (tag, condition_resolved), + // Native `GetTagInfo(...) or last`: no matching alternative ends + // this serial directory rather than falling through to its next + // numeric key. + Selection::NoMatch => { + result.no_matching_alternative += 1; + break; + } + Selection::UnavailableContext => { + result.unavailable_condition_context += 1; + result.tainted = true; + break; + } + Selection::RefusedCondition => { + result.omitted += 1; + result.tainted = true; + break; + } + }; + + // A native Hook executes before ProcessSerialData computes this + // field's length and calls ReadValue. Reading with the generated + // operands first would manufacture both this value and every later + // cursor position, so a malformed artifact cannot defer this refusal + // until report time. + if tag.omitted.hook { + result.omitted += 1; + result.tainted = true; + break; + } + + let more = match dir.dir_len.checked_sub(pos) { + Some(more) => more, + None => break, + }; + // Version 1 deliberately models only the proven scalar and string + // operand family used by the gate-clear Real Audio tables. An + // artifact must not promote another common `Fmt` merely because the + // shared low-level decoder happens to know how to read it. + if !serial_format_supported(tag.format.format) { + result.malformed_layout += 1; + result.tainted = true; + break; + } + let count = match count_for(tag, &prior_raw, more) { + Some(count) => count, + None => { + result.unavailable_prior_raw += 1; + result.tainted = true; + break; + } + }; + let Some(len) = usize::try_from(tag.format.format.size()) + .ok() + .and_then(|size| size.checked_mul(count)) + else { + result.malformed_layout += 1; + result.tainted = true; + break; + }; + // ProcessSerialData checks its requested span before ReadValue, so a + // short final scalar stops the directory instead of becoming a + // shortened value (the general read port still owns its own string + // shortening rules for a valid request). + if match pos.checked_add(len) { + Some(end) => end > dir.dir_len, + None => true, + } { + break; + } + let Some(offset) = dir.dir_start.checked_add(pos) else { + result.unreadable_value += 1; + break; + }; + let Some(more_i64) = i64::try_from(more).ok() else { + result.malformed_layout += 1; + result.tainted = true; + break; + }; + let Some(raw) = engine::read_value( + dir.data, + offset, + tag.format.format, + count, + more_i64, + dir.byte_order, + ) else { + result.unreadable_value += 1; + break; + }; + prior_raw.insert(entry.serial_index, raw.clone()); + + // No serial SubDirectory is currently represented. It would run + // after raw storage and before FoundTag, so an omitted edge taints the + // remaining directory rather than allowing later state to look safe. + if tag.omitted.subdirectory { + result.omitted += 1; + result.tainted = true; + break; + } + // ProcessSerialData still remembers a zero-length ReadValue result, + // but its `FoundTag` call is guarded by `if $count`; RawConv and + // reporting therefore do not run for this slot. + if count == 0 { + pos += len; + continue; + } + // The processor retains Unknown rows for index synchronization, but + // never invokes FoundTag (and therefore never RawConv) unless Unknown + // output was requested by its caller. + if tag.flags.unknown && !sink.unknown_enabled() { + pos += len; + continue; + } + if emit_selected(table, tag, condition_resolved, raw, ctx, sink, &mut result) { + break; + } + pos += len; + } + result +} + +fn select(entry: &SerialEntry, ctx: &mut Ctx) -> Selection { + for tag in entry.alternatives { + if tag.condition.is_none() && tag.omitted.condition { + return Selection::RefusedCondition; + } + let Some(condition) = tag.condition else { + return Selection::Selected { + tag, + condition_resolved: false, + }; + }; + // ProcessSerialData calls GetTagInfo before it has `$format`, `$count` + // or a value pointer. This staged reader has no faithful retry path; + // its generator must gate such rows, and this catches a bad artifact. + if condition.needs_value_context() { + return Selection::UnavailableContext; + } + if condition.eval(ctx) { + return Selection::Selected { + tag, + condition_resolved: true, + }; + } + } + Selection::NoMatch +} + +const fn serial_format_supported(format: super::Fmt) -> bool { + matches!( + format, + super::Fmt::Int8u + | super::Fmt::Int16u + | super::Fmt::Int32u + | super::Fmt::Str(_) + | super::Fmt::Undef(_) + | super::Fmt::RemainderString + ) +} + +fn count_for( + tag: &SerialTag, + prior_raw: &HashMap, + more: usize, +) -> Option { + match tag.format.count { + SerialCount::Fixed { value } => Some(value), + SerialCount::RemainingBytes => Some(more), + SerialCount::PriorRaw { serial_index } => prior_raw + .get(&serial_index)? + .as_integer() + .and_then(|value| usize::try_from(value).ok()), + SerialCount::FloorDivPriorRaw { + serial_index, + add, + divisor, + } => prior_raw + .get(&serial_index)? + .as_integer() + .and_then(|value| usize::try_from(value).ok())? + .checked_add(add) + .and_then(|value| value.checked_div(divisor)), + } +} + +/// Returns true when a state-affecting refusal makes the remaining directory +/// unsafe to interpret. +fn emit_selected( + table: &'static SerialTable, + tag: &'static SerialTag, + condition_resolved: bool, + raw: DecodedValue, + ctx: &mut Ctx, + sink: &mut dyn SerialEmissionSink, + result: &mut SerialWalkResult, +) -> bool { + let mut omitted = tag.omitted; + if condition_resolved { + omitted.condition = false; + } + match tag.raw_conv { + Some(super::RawConvEffect::SetMember { member }) => { + let Some(value) = engine::member_value(&raw) else { + result.omitted += 1; + result.tainted = true; + return true; + }; + ctx.members.insert(member, value); + omitted.raw_conv = false; + } + Some(super::RawConvEffect::ValueLocal) | None => {} + } + if omitted.raw_conv && tag.raw_conv.is_none() { + result.omitted += 1; + result.tainted = true; + return true; + } + if omitted.any() { + result.omitted += 1; + return false; + } + let Some(converted) = runtime::apply_value_conv(tag.value_conv, &raw) else { + result.omitted += 1; + return false; + }; + let (value, value_conv) = if tag.flags.binary && tag.value_conv.is_none() { + let Some(length) = perl_length(&raw) else { + result.omitted += 1; + return false; + }; + ( + TagValue::String(format!( + "(Binary data {length} bytes, use -b option to extract)" + )), + None, + ) + } else { + let unconverted = || { + if tag.flags.list { + runtime::to_tag_value(&converted) + } else { + runtime::to_exiftool_value(&converted) + } + }; + match runtime::render(tag.print_conv, &converted) { + Some(rendered) => (TagValue::String(rendered), Some(unconverted())), + None => (unconverted(), None), + } + }; + sink.emit(Emitted { + module: table.module, + table: table.table, + group0: tag.groups.g0.unwrap_or(table.group0), + group1: tag.groups.g1.unwrap_or(table.group1), + group2: tag.groups.g2.unwrap_or(table.group2), + name: tag.name, + value, + value_conv, + low_priority: low_priority(tag.flags), + avoid: tag.flags.avoid, + rational: None, + }); + result.emitted += 1; + false +} + +fn low_priority(flags: IfdFlags) -> bool { + flags.priority.or(if flags.avoid { Some(0) } else { None }) == Some(0) +} + +fn perl_length(value: &DecodedValue) -> Option { + match value { + DecodedValue::Undefined(bytes) | DecodedValue::StringBytes(bytes) => Some(bytes.len()), + DecodedValue::String(value) => Some(value.len()), + other => other.perl_string().map(|value| value.len()), + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use super::*; + use crate::exiftool_tables::{ + Cond, GateA, IfdFlags, Omitted, PrintConv, RawConvEffect, SerialCount, SerialEntry, + SerialFormat, SerialProcessorFacts, SerialTable, SerialTag, TagGroups, + }; + + static PROCESSOR: SerialProcessorFacts = SerialProcessorFacts { + name: "Image::ExifTool::Shared::ProcessSerialData", + source_file: "Image/ExifTool/Shared.pm", + source_sha256: "source", + source_body_sha256: "body", + }; + static EMPTY_GATE: GateA = GateA { blocked_by: &[] }; + static BLOCKED_GATE: GateA = GateA { + blocked_by: &[("serial_row_shape", 1)], + }; + static PLAIN: SerialTag = SerialTag { + name: "Count", + format: SerialFormat { + format: super::super::Fmt::Int8u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }; + static PRIOR: SerialTag = SerialTag { + name: "Values", + format: SerialFormat { + format: super::super::Fmt::Int16u, + count: SerialCount::PriorRaw { serial_index: 0 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }; + static UNKNOWN: SerialTag = SerialTag { + name: "UnknownCount", + format: SerialFormat { + format: super::super::Fmt::Int8u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + ..IfdFlags::NONE + }, + raw_conv: Some(RawConvEffect::SetMember { + member: "UnknownSeen", + }), + omitted: Omitted { + raw_conv: true, + ..Omitted::NONE + }, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }; + static MEMBER: SerialTag = SerialTag { + name: "State", + format: SerialFormat { + format: super::super::Fmt::Int8u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: Some(RawConvEffect::SetMember { member: "Seen" }), + omitted: Omitted { + raw_conv: true, + ..Omitted::NONE + }, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }; + static AFTER_MEMBER: SerialTag = SerialTag { + name: "AfterState", + format: SerialFormat { + format: super::super::Fmt::Int8u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: Some(Cond::MemberCmp { + member: "Seen", + op: super::super::CmpOp::Eq, + value: 7, + }), + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted { + condition: true, + ..Omitted::NONE + }, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }; + static NEVER: SerialTag = SerialTag { + name: "Never", + format: SerialFormat { + format: super::super::Fmt::Int8u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: Some(Cond::MemberTruthy { + member: "missing", + negate: false, + }), + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }; + static NON_NUMERIC: SerialTag = SerialTag { + name: "NonNumericCount", + format: SerialFormat { + format: super::super::Fmt::Str(1), + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }; + static NEEDS_CONTEXT: SerialTag = SerialTag { + name: "NeedsContext", + format: SerialFormat { + format: super::super::Fmt::Int8u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: Some(Cond::CountCmp { + op: super::super::CmpOp::Eq, + value: 1, + }), + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }; + static COUNTED_STRING: SerialTag = SerialTag { + name: "CountedString", + format: SerialFormat { + format: super::super::Fmt::Str(1), + count: SerialCount::PriorRaw { serial_index: 0 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }; + static HOOK: SerialTag = SerialTag { + name: "Hooked", + format: SerialFormat { + format: super::super::Fmt::Int8u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted { + hook: true, + ..Omitted::NONE + }, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }; + static UNSUPPORTED_FORMAT: SerialTag = SerialTag { + name: "UnprovenFormat", + format: SerialFormat { + format: super::super::Fmt::Int8s, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }; + static REMAINDER: SerialTag = SerialTag { + name: "Remainder", + format: SerialFormat { + format: super::super::Fmt::RemainderString, + count: SerialCount::RemainingBytes, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }; + static ENTRY_COUNT: [SerialTag; 1] = [PLAIN]; + static ENTRY_VALUES: [SerialTag; 1] = [PRIOR]; + static ENTRY_UNKNOWN: [SerialTag; 1] = [UNKNOWN]; + static ENTRY_MEMBER: [SerialTag; 1] = [MEMBER]; + static ENTRY_AFTER_MEMBER: [SerialTag; 1] = [AFTER_MEMBER]; + static ENTRY_NEVER: [SerialTag; 1] = [NEVER]; + static ENTRY_NON_NUMERIC: [SerialTag; 1] = [NON_NUMERIC]; + static ENTRY_NEEDS_CONTEXT: [SerialTag; 1] = [NEEDS_CONTEXT]; + static ENTRY_COUNTED_STRING: [SerialTag; 1] = [COUNTED_STRING]; + static ENTRY_HOOK: [SerialTag; 1] = [HOOK]; + static ENTRY_UNSUPPORTED_FORMAT: [SerialTag; 1] = [UNSUPPORTED_FORMAT]; + static ENTRY_REMAINDER: [SerialTag; 1] = [REMAINDER]; + static TWO_ENTRIES: [SerialEntry; 2] = [ + SerialEntry { + serial_index: 0, + alternatives: &ENTRY_COUNT, + }, + SerialEntry { + serial_index: 1, + alternatives: &ENTRY_VALUES, + }, + ]; + static UNKNOWN_ENTRIES: [SerialEntry; 2] = [ + SerialEntry { + serial_index: 0, + alternatives: &ENTRY_UNKNOWN, + }, + SerialEntry { + serial_index: 1, + alternatives: &ENTRY_VALUES, + }, + ]; + static STATE_ENTRIES: [SerialEntry; 2] = [ + SerialEntry { + serial_index: 0, + alternatives: &ENTRY_MEMBER, + }, + SerialEntry { + serial_index: 1, + alternatives: &ENTRY_AFTER_MEMBER, + }, + ]; + static STOP_ENTRIES: [SerialEntry; 2] = [ + SerialEntry { + serial_index: 0, + alternatives: &ENTRY_NEVER, + }, + SerialEntry { + serial_index: 1, + alternatives: &ENTRY_COUNT, + }, + ]; + static NON_NUMERIC_ENTRIES: [SerialEntry; 2] = [ + SerialEntry { + serial_index: 0, + alternatives: &ENTRY_NON_NUMERIC, + }, + SerialEntry { + serial_index: 1, + alternatives: &ENTRY_VALUES, + }, + ]; + static CONTEXT_ENTRIES: [SerialEntry; 1] = [SerialEntry { + serial_index: 0, + alternatives: &ENTRY_NEEDS_CONTEXT, + }]; + static COUNTED_STRING_ENTRIES: [SerialEntry; 2] = [ + SerialEntry { + serial_index: 0, + alternatives: &ENTRY_COUNT, + }, + SerialEntry { + serial_index: 1, + alternatives: &ENTRY_COUNTED_STRING, + }, + ]; + static HOOK_ENTRIES: [SerialEntry; 1] = [SerialEntry { + serial_index: 0, + alternatives: &ENTRY_HOOK, + }]; + static UNSUPPORTED_FORMAT_ENTRIES: [SerialEntry; 1] = [SerialEntry { + serial_index: 0, + alternatives: &ENTRY_UNSUPPORTED_FORMAT, + }]; + static REMAINDER_ENTRIES: [SerialEntry; 1] = [SerialEntry { + serial_index: 0, + alternatives: &ENTRY_REMAINDER, + }]; + static TABLE: SerialTable = SerialTable { + module: "Shared", + table: "Serial", + group0: "MakerNotes", + group1: "Shared", + group2: "Camera", + default_format: super::super::Fmt::Int8u, + processor: PROCESSOR, + gate_a: EMPTY_GATE, + entries: &TWO_ENTRIES, + }; + + #[derive(Default)] + struct Sink { + rows: Vec, + enabled: bool, + unknown: bool, + } + impl SerialEmissionSink for Sink { + fn emit(&mut self, row: Emitted) { + self.rows.push(row); + } + fn serial_enabled(&self, _: &'static SerialTable) -> bool { + self.enabled + } + fn unknown_enabled(&self) -> bool { + self.unknown + } + } + + fn walk( + table: &'static SerialTable, + data: &[u8], + sink: &mut Sink, + members: &mut HashMap<&'static str, super::super::MemberValue>, + ) -> SerialWalkResult { + process_serial_directory( + table, + SerialDir { + data, + dir_start: 0, + dir_len: data.len(), + base: 0, + data_pos: 0, + byte_order: ByteOrder::Little, + }, + &mut Ctx::new(members), + sink, + ) + } + + #[test] + fn gate_b_blocks_before_reading_any_slot() { + let mut sink = Sink::default(); + let mut members = HashMap::new(); + let result = walk(&TABLE, &[2, 1, 0, 2, 0], &mut sink, &mut members); + assert_eq!(result.gate_b_blocked, 1); + assert!(sink.rows.is_empty()); + } + + #[test] + fn prior_raw_count_drives_following_read_before_rendering() { + let mut sink = Sink { + enabled: true, + ..Sink::default() + }; + let mut members = HashMap::new(); + let result = walk(&TABLE, &[2, 1, 0, 2, 0], &mut sink, &mut members); + assert_eq!(result.emitted, 2); + assert_eq!(sink.rows[0].value, TagValue::Integer(2)); + assert_eq!(sink.rows[1].value, TagValue::String("1 2".to_owned())); + } + + #[test] + fn unknown_row_keeps_cursor_but_does_not_report_or_apply_foundtag_work() { + let table = SerialTable { + entries: &UNKNOWN_ENTRIES, + ..TABLE + }; + let table: &'static SerialTable = Box::leak(Box::new(table)); + let mut sink = Sink { + enabled: true, + ..Sink::default() + }; + let mut members = HashMap::new(); + let result = walk(table, &[2, 8, 0, 9, 0], &mut sink, &mut members); + assert_eq!(result.emitted, 1); + assert_eq!(sink.rows[0].name, "Values"); + assert_eq!(sink.rows[0].value, TagValue::String("8 9".to_owned())); + assert!(!members.contains_key("UnknownSeen")); + } + + #[test] + fn raw_state_is_saved_before_later_condition_selects() { + let table = SerialTable { + entries: &STATE_ENTRIES, + ..TABLE + }; + let table: &'static SerialTable = Box::leak(Box::new(table)); + let mut sink = Sink { + enabled: true, + ..Sink::default() + }; + let mut members = HashMap::new(); + let result = walk(table, &[7, 3], &mut sink, &mut members); + assert_eq!(result.emitted, 2); + assert_eq!(sink.rows[1].name, "AfterState"); + } + + #[test] + fn no_matching_alternative_stops_before_later_slots() { + let table = SerialTable { + entries: &STOP_ENTRIES, + ..TABLE + }; + let table: &'static SerialTable = Box::leak(Box::new(table)); + let mut sink = Sink { + enabled: true, + ..Sink::default() + }; + let mut members = HashMap::new(); + let result = walk(table, &[7, 3], &mut sink, &mut members); + assert_eq!(result.no_matching_alternative, 1); + assert!(sink.rows.is_empty()); + } + + #[test] + fn gate_a_blocks_the_entire_directory() { + let table = SerialTable { + gate_a: BLOCKED_GATE, + ..TABLE + }; + let table: &'static SerialTable = Box::leak(Box::new(table)); + let mut sink = Sink { + enabled: true, + ..Sink::default() + }; + let mut members = HashMap::new(); + let result = walk(table, &[2, 1, 0, 2, 0], &mut sink, &mut members); + assert_eq!(result.gate_a_blocked, 1); + assert!(sink.rows.is_empty()); + } + + #[test] + fn nonnumeric_prior_raw_count_is_refused_without_coercion() { + let table = SerialTable { + entries: &NON_NUMERIC_ENTRIES, + ..TABLE + }; + let table: &'static SerialTable = Box::leak(Box::new(table)); + let mut sink = Sink { + enabled: true, + ..Sink::default() + }; + let mut members = HashMap::new(); + let result = walk(table, b"x\0\0", &mut sink, &mut members); + assert_eq!(result.unavailable_prior_raw, 1); + assert!(result.tainted); + assert_eq!(sink.rows.len(), 1); + assert_eq!(sink.rows[0].name, "NonNumericCount"); + } + + #[test] + fn unavailable_gettaginfo_context_taints_before_reading() { + let table = SerialTable { + entries: &CONTEXT_ENTRIES, + ..TABLE + }; + let table: &'static SerialTable = Box::leak(Box::new(table)); + let mut sink = Sink { + enabled: true, + ..Sink::default() + }; + let mut members = HashMap::new(); + let result = walk(table, &[1], &mut sink, &mut members); + assert_eq!(result.unavailable_condition_context, 1); + assert!(result.tainted); + assert!(sink.rows.is_empty()); + } + + #[test] + fn dynamic_string_count_uses_prior_raw_slot_and_truncates_at_nul() { + let table = SerialTable { + entries: &COUNTED_STRING_ENTRIES, + ..TABLE + }; + let table: &'static SerialTable = Box::leak(Box::new(table)); + let mut sink = Sink { + enabled: true, + ..Sink::default() + }; + let mut members = HashMap::new(); + let result = walk(table, b"\x04ok\0x", &mut sink, &mut members); + assert_eq!(result.emitted, 2); + assert_eq!(sink.rows[1].name, "CountedString"); + assert_eq!(sink.rows[1].value, TagValue::String("ok".to_owned())); + } + + #[test] + fn omitted_hook_taints_before_reading_or_storing_any_value() { + let table = SerialTable { + entries: &HOOK_ENTRIES, + ..TABLE + }; + let table: &'static SerialTable = Box::leak(Box::new(table)); + let mut sink = Sink { + enabled: true, + ..Sink::default() + }; + let mut members = HashMap::new(); + let result = walk(table, &[7], &mut sink, &mut members); + assert_eq!(result.omitted, 1); + assert!(result.tainted); + assert!(sink.rows.is_empty()); + assert!(members.is_empty()); + } + + #[test] + fn signed_format_is_refused_before_it_can_supply_a_dynamic_count() { + let table = SerialTable { + entries: &UNSUPPORTED_FORMAT_ENTRIES, + ..TABLE + }; + let table: &'static SerialTable = Box::leak(Box::new(table)); + let mut sink = Sink { + enabled: true, + ..Sink::default() + }; + let mut members = HashMap::new(); + let result = walk(table, &[0, 0, 0, 0], &mut sink, &mut members); + assert_eq!(result.malformed_layout, 1); + assert!(result.tainted); + assert!(sink.rows.is_empty()); + } + + #[test] + fn zero_length_bare_string_is_not_reported() { + let table = SerialTable { + entries: &REMAINDER_ENTRIES, + ..TABLE + }; + let table: &'static SerialTable = Box::leak(Box::new(table)); + let mut sink = Sink { + enabled: true, + ..Sink::default() + }; + let mut members = HashMap::new(); + let result = walk(table, b"", &mut sink, &mut members); + assert_eq!(result.entries_seen, 1); + assert_eq!(result.emitted, 0); + assert!(sink.rows.is_empty()); + } + + #[test] + fn bare_string_consumes_the_remaining_bytes_and_truncates_at_nul() { + let table = SerialTable { + entries: &REMAINDER_ENTRIES, + ..TABLE + }; + let table: &'static SerialTable = Box::leak(Box::new(table)); + let mut sink = Sink { + enabled: true, + ..Sink::default() + }; + let mut members = HashMap::new(); + let result = walk(table, b"raw\0ignored", &mut sink, &mut members); + assert_eq!(result.emitted, 1); + assert_eq!(sink.rows[0].value, TagValue::String("raw".to_owned())); + } +} diff --git a/src/exiftool_tables/serial_schema.rs b/src/exiftool_tables/serial_schema.rs new file mode 100644 index 000000000..8af82f3cb --- /dev/null +++ b/src/exiftool_tables/serial_schema.rs @@ -0,0 +1,114 @@ +//! Generated schema for ExifTool `ProcessSerialData` tables. +//! +//! The native processor walks numeric table keys in order while carrying raw +//! values from earlier slots into later `Format => "type[$val{N}]"` count +//! expressions. This module is deliberately only the data contract. A +//! generated table is not a production route; [`super::serial_engine`] still +//! requires a caller-supplied Gate B policy before it reads any bytes. + +use super::{Cond, ExprId, Fmt, GateA, IfdFlags, Omitted, PrintConv, RawConvEffect, TagGroups}; + +/// Source provenance for the shared native `ProcessSerialData` body selected +/// by a table. The generator obtains these facts from the captured CODE ref; +/// the reader never dispatches on a module, table, or processor name. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SerialProcessorFacts { + pub name: &'static str, + pub source_file: &'static str, + pub source_sha256: &'static str, + pub source_body_sha256: &'static str, +} + +/// A generated native serial table. +#[derive(Clone, Copy, Debug)] +pub struct SerialTable { + pub module: &'static str, + pub table: &'static str, + pub group0: &'static str, + pub group1: &'static str, + pub group2: &'static str, + /// The table `FORMAT` after ExifTool's defaulting. Every entry also + /// carries its resolved runtime format so this remains audit data rather + /// than an implicit reader fallback. + pub default_format: Fmt, + pub processor: SerialProcessorFacts, + /// Aggregate table and row refusals. A nonempty gate prevents partial + /// execution: a skipped serial slot would change all later offsets. + pub gate_a: GateA, + /// Sorted by `serial_index`; a gap is malformed for this processor. + pub entries: &'static [SerialEntry], +} + +/// One native numeric key in a serial table. +#[derive(Clone, Copy, Debug)] +pub struct SerialEntry { + pub serial_index: usize, + /// Native `_variants` in source order. A scalar source row has one + /// unconditional alternative. + pub alternatives: &'static [SerialTag], +} + +/// The count expression ProcessSerialData evaluates after GetTagInfo chose a +/// row but before `ReadValue` runs. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SerialCount { + Fixed { + value: usize, + }, + PriorRaw { + serial_index: usize, + }, + FloorDivPriorRaw { + serial_index: usize, + add: usize, + divisor: usize, + }, + /// Explicit per-field bare `Format => 'string'`: consume `$size - $pos`. + RemainingBytes, +} + +/// A selected serial row's resolved read operands. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SerialFormat { + pub format: Fmt, + pub count: SerialCount, +} + +/// One source alternative selected by `GetTagInfo`. +#[derive(Clone, Copy, Debug)] +pub struct SerialTag { + pub name: &'static str, + pub format: SerialFormat, + /// `None` is unconditional only when `omitted.condition` is false. + pub condition: Option, + pub flags: IfdFlags, + pub raw_conv: Option, + pub omitted: Omitted, + pub value_conv: Option, + pub print_conv: PrintConv, + pub groups: TagGroups, +} + +/// A native source alternative deliberately not represented by [`SerialTag`]. +/// +/// The generator emits one record for every withheld alternative; this keeps +/// source inventory and generated output independently reconcilable. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct OmittedSerialNativeRow { + pub module: &'static str, + pub table: &'static str, + pub serial_index: usize, + pub variant: bool, + pub alternative: usize, + pub name: Option<&'static str>, + pub reasons: &'static [&'static str], +} + +/// A selected native serial table with a table-level refusal or no generated +/// descriptor. Row omissions belong in [`OmittedSerialNativeRow`]. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct OmittedSerialNativeTable { + pub module: &'static str, + pub table: &'static str, + pub reasons: &'static [&'static str], +} diff --git a/src/exiftool_tables/serial_tables.rs b/src/exiftool_tables/serial_tables.rs new file mode 100644 index 000000000..a4d45b8ba --- /dev/null +++ b/src/exiftool_tables/serial_tables.rs @@ -0,0 +1,2657 @@ +//! Generated serial-directory facts; no reader or route is activated by this file. +use super::*; +pub static SERIAL_CANON_AFINFO: SerialTable = SerialTable { + module: "Canon", + table: "AFInfo", + group0: "MakerNotes", + group1: "Canon", + group2: "Camera", + default_format: Fmt::Int16u, + processor: SerialProcessorFacts { + name: "Image::ExifTool::Canon::ProcessSerialData", + source_file: "Image/ExifTool/Canon.pm", + source_sha256: "d22b110e7b2e1af2d6e59bb97db97a04ffc2bfd1ba7d078d380ae60fbc70bbba", + source_body_sha256: "ab5cb31e06a991a02f5569f1c008303c0505d28caf7ae2ec044aeede9c9f999c", + }, + gate_a: GateA { + blocked_by: &[ + ("serial_condition_missing_member", 2), + ("serial_decode_bits_words", 1), + ("serial_emitter_format", 3), + ], + }, + entries: &[ + SerialEntry { + serial_index: 0, + alternatives: &[SerialTag { + name: "NumAFPoints", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 1, + alternatives: &[SerialTag { + name: "ValidAFPoints", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 2, + alternatives: &[SerialTag { + name: "CanonImageWidth", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups { + g0: None, + g1: None, + g2: Some("Image"), + }, + }], + }, + SerialEntry { + serial_index: 3, + alternatives: &[SerialTag { + name: "CanonImageHeight", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups { + g0: None, + g1: None, + g2: Some("Image"), + }, + }], + }, + SerialEntry { + serial_index: 4, + alternatives: &[SerialTag { + name: "AFImageWidth", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 5, + alternatives: &[SerialTag { + name: "AFImageHeight", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 6, + alternatives: &[SerialTag { + name: "AFAreaWidth", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 7, + alternatives: &[SerialTag { + name: "AFAreaHeight", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 12, + alternatives: &[SerialTag { + name: "PrimaryAFPoint", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + ], +}; +pub static SERIAL_CANON_AFINFO2: SerialTable = SerialTable { + module: "Canon", + table: "AFInfo2", + group0: "MakerNotes", + group1: "Canon", + group2: "Camera", + default_format: Fmt::Int16u, + processor: SerialProcessorFacts { + name: "Image::ExifTool::Canon::ProcessSerialData", + source_file: "Image/ExifTool/Canon.pm", + source_sha256: "d22b110e7b2e1af2d6e59bb97db97a04ffc2bfd1ba7d078d380ae60fbc70bbba", + source_body_sha256: "ab5cb31e06a991a02f5569f1c008303c0505d28caf7ae2ec044aeede9c9f999c", + }, + gate_a: GateA { + blocked_by: &[ + ("serial_condition_missing_member", 2), + ("serial_decode_bits_words", 2), + ("serial_emitter_format", 7), + ("serial_print_conv", 1), + ("serial_raw_conv", 1), + ("serial_row_shape", 1), + ], + }, + entries: &[ + SerialEntry { + serial_index: 0, + alternatives: &[SerialTag { + name: "AFInfoSize", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 3, + alternatives: &[SerialTag { + name: "ValidAFPoints", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 4, + alternatives: &[SerialTag { + name: "CanonImageWidth", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups { + g0: None, + g1: None, + g2: Some("Image"), + }, + }], + }, + SerialEntry { + serial_index: 5, + alternatives: &[SerialTag { + name: "CanonImageHeight", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups { + g0: None, + g1: None, + g2: Some("Image"), + }, + }], + }, + SerialEntry { + serial_index: 6, + alternatives: &[SerialTag { + name: "AFImageWidth", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 7, + alternatives: &[SerialTag { + name: "AFImageHeight", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + ], +}; +pub static SERIAL_REAL_AUDIOV3: SerialTable = SerialTable { + module: "Real", + table: "AudioV3", + group0: "Real", + group1: "Real-RA3", + group2: "Audio", + default_format: Fmt::Int8u, + processor: SerialProcessorFacts { + name: "Image::ExifTool::Canon::ProcessSerialData", + source_file: "Image/ExifTool/Canon.pm", + source_sha256: "d22b110e7b2e1af2d6e59bb97db97a04ffc2bfd1ba7d078d380ae60fbc70bbba", + source_body_sha256: "ab5cb31e06a991a02f5569f1c008303c0505d28caf7ae2ec044aeede9c9f999c", + }, + gate_a: GateA { blocked_by: &[] }, + entries: &[ + SerialEntry { + serial_index: 0, + alternatives: &[SerialTag { + name: "Channels", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 1, + alternatives: &[SerialTag { + name: "Unknown", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 3 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 2, + alternatives: &[SerialTag { + name: "BytesPerMinute", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 3, + alternatives: &[SerialTag { + name: "AudioBytes", + format: SerialFormat { + format: Fmt::Int32u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 4, + alternatives: &[SerialTag { + name: "TitleLen", + format: SerialFormat { + format: Fmt::Int8u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 5, + alternatives: &[SerialTag { + name: "Title", + format: SerialFormat { + format: Fmt::Str(1), + count: SerialCount::PriorRaw { serial_index: 4 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 6, + alternatives: &[SerialTag { + name: "ArtistLen", + format: SerialFormat { + format: Fmt::Int8u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 7, + alternatives: &[SerialTag { + name: "Artist", + format: SerialFormat { + format: Fmt::Str(1), + count: SerialCount::PriorRaw { serial_index: 6 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups { + g0: None, + g1: None, + g2: Some("Author"), + }, + }], + }, + SerialEntry { + serial_index: 8, + alternatives: &[SerialTag { + name: "CopyrightLen", + format: SerialFormat { + format: Fmt::Int8u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 9, + alternatives: &[SerialTag { + name: "Copyright", + format: SerialFormat { + format: Fmt::Str(1), + count: SerialCount::PriorRaw { serial_index: 8 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups { + g0: None, + g1: None, + g2: Some("Author"), + }, + }], + }, + SerialEntry { + serial_index: 10, + alternatives: &[SerialTag { + name: "CommentLen", + format: SerialFormat { + format: Fmt::Int8u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 11, + alternatives: &[SerialTag { + name: "Comment", + format: SerialFormat { + format: Fmt::Str(1), + count: SerialCount::PriorRaw { serial_index: 10 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + ], +}; +pub static SERIAL_REAL_AUDIOV4: SerialTable = SerialTable { + module: "Real", + table: "AudioV4", + group0: "Real", + group1: "Real-RA4", + group2: "Audio", + default_format: Fmt::Int16u, + processor: SerialProcessorFacts { + name: "Image::ExifTool::Canon::ProcessSerialData", + source_file: "Image/ExifTool/Canon.pm", + source_sha256: "d22b110e7b2e1af2d6e59bb97db97a04ffc2bfd1ba7d078d380ae60fbc70bbba", + source_body_sha256: "ab5cb31e06a991a02f5569f1c008303c0505d28caf7ae2ec044aeede9c9f999c", + }, + gate_a: GateA { blocked_by: &[] }, + entries: &[ + SerialEntry { + serial_index: 0, + alternatives: &[SerialTag { + name: "FourCC1", + format: SerialFormat { + format: Fmt::Undef(1), + count: SerialCount::Fixed { value: 4 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 1, + alternatives: &[SerialTag { + name: "AudioFileSize", + format: SerialFormat { + format: Fmt::Int32u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 2, + alternatives: &[SerialTag { + name: "Version2", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 3, + alternatives: &[SerialTag { + name: "HeaderSize", + format: SerialFormat { + format: Fmt::Int32u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 4, + alternatives: &[SerialTag { + name: "CodecFlavorID", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 5, + alternatives: &[SerialTag { + name: "CodedFrameSize", + format: SerialFormat { + format: Fmt::Int32u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 6, + alternatives: &[SerialTag { + name: "AudioBytes", + format: SerialFormat { + format: Fmt::Int32u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 7, + alternatives: &[SerialTag { + name: "BytesPerMinute", + format: SerialFormat { + format: Fmt::Int32u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 8, + alternatives: &[SerialTag { + name: "Unknown", + format: SerialFormat { + format: Fmt::Int32u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 9, + alternatives: &[SerialTag { + name: "SubPacketH", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 10, + alternatives: &[SerialTag { + name: "AudioFrameSize", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 11, + alternatives: &[SerialTag { + name: "SubPacketSize", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 12, + alternatives: &[SerialTag { + name: "Unknown", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 13, + alternatives: &[SerialTag { + name: "SampleRate", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 14, + alternatives: &[SerialTag { + name: "Unknown", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 15, + alternatives: &[SerialTag { + name: "BitsPerSample", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 16, + alternatives: &[SerialTag { + name: "Channels", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 17, + alternatives: &[SerialTag { + name: "FourCC2Len", + format: SerialFormat { + format: Fmt::Int8u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 18, + alternatives: &[SerialTag { + name: "FourCC2", + format: SerialFormat { + format: Fmt::Undef(1), + count: SerialCount::Fixed { value: 4 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 19, + alternatives: &[SerialTag { + name: "FourCC3Len", + format: SerialFormat { + format: Fmt::Int8u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 20, + alternatives: &[SerialTag { + name: "FourCC3", + format: SerialFormat { + format: Fmt::Undef(1), + count: SerialCount::Fixed { value: 4 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 21, + alternatives: &[SerialTag { + name: "Unknown", + format: SerialFormat { + format: Fmt::Int8u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 22, + alternatives: &[SerialTag { + name: "Unknown", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 23, + alternatives: &[SerialTag { + name: "TitleLen", + format: SerialFormat { + format: Fmt::Int8u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 24, + alternatives: &[SerialTag { + name: "Title", + format: SerialFormat { + format: Fmt::Str(1), + count: SerialCount::PriorRaw { serial_index: 23 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 25, + alternatives: &[SerialTag { + name: "ArtistLen", + format: SerialFormat { + format: Fmt::Int8u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 26, + alternatives: &[SerialTag { + name: "Artist", + format: SerialFormat { + format: Fmt::Str(1), + count: SerialCount::PriorRaw { serial_index: 25 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups { + g0: None, + g1: None, + g2: Some("Author"), + }, + }], + }, + SerialEntry { + serial_index: 27, + alternatives: &[SerialTag { + name: "CopyrightLen", + format: SerialFormat { + format: Fmt::Int8u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 28, + alternatives: &[SerialTag { + name: "Copyright", + format: SerialFormat { + format: Fmt::Str(1), + count: SerialCount::PriorRaw { serial_index: 27 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups { + g0: None, + g1: None, + g2: Some("Author"), + }, + }], + }, + SerialEntry { + serial_index: 29, + alternatives: &[SerialTag { + name: "CommentLen", + format: SerialFormat { + format: Fmt::Int8u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 30, + alternatives: &[SerialTag { + name: "Comment", + format: SerialFormat { + format: Fmt::Str(1), + count: SerialCount::PriorRaw { serial_index: 29 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + ], +}; +pub static SERIAL_REAL_AUDIOV5: SerialTable = SerialTable { + module: "Real", + table: "AudioV5", + group0: "Real", + group1: "Real-RA5", + group2: "Audio", + default_format: Fmt::Int16u, + processor: SerialProcessorFacts { + name: "Image::ExifTool::Canon::ProcessSerialData", + source_file: "Image/ExifTool/Canon.pm", + source_sha256: "d22b110e7b2e1af2d6e59bb97db97a04ffc2bfd1ba7d078d380ae60fbc70bbba", + source_body_sha256: "ab5cb31e06a991a02f5569f1c008303c0505d28caf7ae2ec044aeede9c9f999c", + }, + gate_a: GateA { blocked_by: &[] }, + entries: &[ + SerialEntry { + serial_index: 0, + alternatives: &[SerialTag { + name: "FourCC1", + format: SerialFormat { + format: Fmt::Undef(1), + count: SerialCount::Fixed { value: 4 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 1, + alternatives: &[SerialTag { + name: "AudioFileSize", + format: SerialFormat { + format: Fmt::Int32u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 2, + alternatives: &[SerialTag { + name: "Version2", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 3, + alternatives: &[SerialTag { + name: "HeaderSize", + format: SerialFormat { + format: Fmt::Int32u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 4, + alternatives: &[SerialTag { + name: "CodecFlavorID", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 5, + alternatives: &[SerialTag { + name: "CodedFrameSize", + format: SerialFormat { + format: Fmt::Int32u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 6, + alternatives: &[SerialTag { + name: "AudioBytes", + format: SerialFormat { + format: Fmt::Int32u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 7, + alternatives: &[SerialTag { + name: "BytesPerMinute", + format: SerialFormat { + format: Fmt::Int32u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 8, + alternatives: &[SerialTag { + name: "Unknown", + format: SerialFormat { + format: Fmt::Int32u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 9, + alternatives: &[SerialTag { + name: "SubPacketH", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 10, + alternatives: &[SerialTag { + name: "FrameSize", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 11, + alternatives: &[SerialTag { + name: "SubPacketSize", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 12, + alternatives: &[SerialTag { + name: "SampleRate", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 13, + alternatives: &[SerialTag { + name: "SampleRate2", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 14, + alternatives: &[SerialTag { + name: "BitsPerSample", + format: SerialFormat { + format: Fmt::Int32u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 15, + alternatives: &[SerialTag { + name: "Channels", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 16, + alternatives: &[SerialTag { + name: "Genr", + format: SerialFormat { + format: Fmt::Int32u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 17, + alternatives: &[SerialTag { + name: "FourCC3", + format: SerialFormat { + format: Fmt::Undef(1), + count: SerialCount::Fixed { value: 4 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + ], +}; +pub static SERIAL_REAL_CONTENTDESCR: SerialTable = SerialTable { + module: "Real", + table: "ContentDescr", + group0: "Real", + group1: "Real-CONT", + group2: "Video", + default_format: Fmt::Int16u, + processor: SerialProcessorFacts { + name: "Image::ExifTool::Canon::ProcessSerialData", + source_file: "Image/ExifTool/Canon.pm", + source_sha256: "d22b110e7b2e1af2d6e59bb97db97a04ffc2bfd1ba7d078d380ae60fbc70bbba", + source_body_sha256: "ab5cb31e06a991a02f5569f1c008303c0505d28caf7ae2ec044aeede9c9f999c", + }, + gate_a: GateA { blocked_by: &[] }, + entries: &[ + SerialEntry { + serial_index: 0, + alternatives: &[SerialTag { + name: "TitleLen", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 1, + alternatives: &[SerialTag { + name: "Title", + format: SerialFormat { + format: Fmt::Str(1), + count: SerialCount::PriorRaw { serial_index: 0 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 2, + alternatives: &[SerialTag { + name: "AuthorLen", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 3, + alternatives: &[SerialTag { + name: "Author", + format: SerialFormat { + format: Fmt::Str(1), + count: SerialCount::PriorRaw { serial_index: 2 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups { + g0: None, + g1: None, + g2: Some("Author"), + }, + }], + }, + SerialEntry { + serial_index: 4, + alternatives: &[SerialTag { + name: "CopyrightLen", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 5, + alternatives: &[SerialTag { + name: "Copyright", + format: SerialFormat { + format: Fmt::Str(1), + count: SerialCount::PriorRaw { serial_index: 4 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups { + g0: None, + g1: None, + g2: Some("Author"), + }, + }], + }, + SerialEntry { + serial_index: 6, + alternatives: &[SerialTag { + name: "CommentLen", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 7, + alternatives: &[SerialTag { + name: "Comment", + format: SerialFormat { + format: Fmt::Str(1), + count: SerialCount::PriorRaw { serial_index: 6 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + ], +}; +pub static SERIAL_REAL_MEDIAPROPS: SerialTable = SerialTable { + module: "Real", + table: "MediaProps", + group0: "Real", + group1: "Real-MDPR", + group2: "Video", + default_format: Fmt::Int32u, + processor: SerialProcessorFacts { + name: "Image::ExifTool::Canon::ProcessSerialData", + source_file: "Image/ExifTool/Canon.pm", + source_sha256: "d22b110e7b2e1af2d6e59bb97db97a04ffc2bfd1ba7d078d380ae60fbc70bbba", + source_body_sha256: "ab5cb31e06a991a02f5569f1c008303c0505d28caf7ae2ec044aeede9c9f999c", + }, + gate_a: GateA { + blocked_by: &[ + ("serial_emitter_format", 1), + ("serial_print_conv_expr", 4), + ("serial_raw_conv", 1), + ("serial_row_shape", 1), + ("serial_table_priority", 1), + ("serial_value_conv", 2), + ], + }, + entries: &[ + SerialEntry { + serial_index: 0, + alternatives: &[SerialTag { + name: "StreamNumber", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 3, + alternatives: &[SerialTag { + name: "StreamMaxPacketSize", + format: SerialFormat { + format: Fmt::Int32u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 4, + alternatives: &[SerialTag { + name: "StreamAvgPacketSize", + format: SerialFormat { + format: Fmt::Int32u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 5, + alternatives: &[SerialTag { + name: "StreamStartTime", + format: SerialFormat { + format: Fmt::Int32u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 8, + alternatives: &[SerialTag { + name: "StreamNameLen", + format: SerialFormat { + format: Fmt::Int8u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 9, + alternatives: &[SerialTag { + name: "StreamName", + format: SerialFormat { + format: Fmt::Str(1), + count: SerialCount::PriorRaw { serial_index: 8 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 10, + alternatives: &[SerialTag { + name: "StreamMimeLen", + format: SerialFormat { + format: Fmt::Int8u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 12, + alternatives: &[SerialTag { + name: "FileInfoLen", + format: SerialFormat { + format: Fmt::Int32u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 13, + alternatives: &[SerialTag { + name: "FileInfoLen2", + format: SerialFormat { + format: Fmt::Int32u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: Some(Cond::MemberStrEq { + member: "RealStreamMime", + value: "logical-fileinfo", + negate: false, + }), + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 14, + alternatives: &[SerialTag { + name: "FileInfoVersion", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 15, + alternatives: &[SerialTag { + name: "PhysicalStreams", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 16, + alternatives: &[SerialTag { + name: "PhysicalStreamNumbers", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::PriorRaw { serial_index: 15 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 17, + alternatives: &[SerialTag { + name: "DataOffsets", + format: SerialFormat { + format: Fmt::Int32u, + count: SerialCount::PriorRaw { serial_index: 15 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 18, + alternatives: &[SerialTag { + name: "NumRules", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 19, + alternatives: &[SerialTag { + name: "PhysicalStreamNumberMap", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::PriorRaw { serial_index: 18 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 20, + alternatives: &[SerialTag { + name: "NumProperties", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + ], +}; +pub static SERIAL_REAL_PROPERTIES: SerialTable = SerialTable { + module: "Real", + table: "Properties", + group0: "Real", + group1: "Real-PROP", + group2: "Video", + default_format: Fmt::Int32u, + processor: SerialProcessorFacts { + name: "Image::ExifTool::Canon::ProcessSerialData", + source_file: "Image/ExifTool/Canon.pm", + source_sha256: "d22b110e7b2e1af2d6e59bb97db97a04ffc2bfd1ba7d078d380ae60fbc70bbba", + source_body_sha256: "ab5cb31e06a991a02f5569f1c008303c0505d28caf7ae2ec044aeede9c9f999c", + }, + gate_a: GateA { + blocked_by: &[ + ("serial_print_conv", 1), + ("serial_print_conv_expr", 4), + ("serial_value_conv", 2), + ], + }, + entries: &[ + SerialEntry { + serial_index: 2, + alternatives: &[SerialTag { + name: "MaxPacketSize", + format: SerialFormat { + format: Fmt::Int32u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 3, + alternatives: &[SerialTag { + name: "AvgPacketSize", + format: SerialFormat { + format: Fmt::Int32u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 4, + alternatives: &[SerialTag { + name: "NumPackets", + format: SerialFormat { + format: Fmt::Int32u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 7, + alternatives: &[SerialTag { + name: "IndexOffset", + format: SerialFormat { + format: Fmt::Int32u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 8, + alternatives: &[SerialTag { + name: "DataOffset", + format: SerialFormat { + format: Fmt::Int32u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags { + unknown: true, + binary: false, + list: false, + protected: false, + avoid: false, + priority: None, + }, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + SerialEntry { + serial_index: 9, + alternatives: &[SerialTag { + name: "NumStreams", + format: SerialFormat { + format: Fmt::Int16u, + count: SerialCount::Fixed { value: 1 }, + }, + condition: None, + flags: IfdFlags::NONE, + raw_conv: None, + omitted: Omitted::NONE, + value_conv: None, + print_conv: PrintConv::None, + groups: TagGroups::NONE, + }], + }, + ], +}; +pub static ALL_SERIAL_TABLES: &[&SerialTable] = &[ + &SERIAL_CANON_AFINFO, + &SERIAL_CANON_AFINFO2, + &SERIAL_REAL_AUDIOV3, + &SERIAL_REAL_AUDIOV4, + &SERIAL_REAL_AUDIOV5, + &SERIAL_REAL_CONTENTDESCR, + &SERIAL_REAL_MEDIAPROPS, + &SERIAL_REAL_PROPERTIES, +]; +pub static OMITTED_SERIAL_NATIVE_ROWS: &[OmittedSerialNativeRow] = &[ + OmittedSerialNativeRow { + module: "Canon", + table: "AFInfo", + serial_index: 8, + variant: false, + alternative: 0, + name: Some("AFAreaXPositions"), + reasons: &["serial_emitter_format"], + }, + OmittedSerialNativeRow { + module: "Canon", + table: "AFInfo", + serial_index: 9, + variant: false, + alternative: 0, + name: Some("AFAreaYPositions"), + reasons: &["serial_emitter_format"], + }, + OmittedSerialNativeRow { + module: "Canon", + table: "AFInfo", + serial_index: 10, + variant: false, + alternative: 0, + name: Some("AFPointsInFocus"), + reasons: &["serial_decode_bits_words", "serial_emitter_format"], + }, + OmittedSerialNativeRow { + module: "Canon", + table: "AFInfo", + serial_index: 11, + variant: true, + alternative: 0, + name: Some("PrimaryAFPoint"), + reasons: &["serial_condition_missing_member"], + }, + OmittedSerialNativeRow { + module: "Canon", + table: "AFInfo", + serial_index: 11, + variant: true, + alternative: 1, + name: Some("Canon_AFInfo_0x000b"), + reasons: &["serial_condition_missing_member"], + }, + OmittedSerialNativeRow { + module: "Canon", + table: "AFInfo2", + serial_index: 1, + variant: false, + alternative: 0, + name: Some("AFAreaMode"), + reasons: &["serial_print_conv"], + }, + OmittedSerialNativeRow { + module: "Canon", + table: "AFInfo2", + serial_index: 2, + variant: false, + alternative: 0, + name: Some("NumAFPoints"), + reasons: &["serial_raw_conv"], + }, + OmittedSerialNativeRow { + module: "Canon", + table: "AFInfo2", + serial_index: 8, + variant: false, + alternative: 0, + name: Some("AFAreaWidths"), + reasons: &["serial_emitter_format"], + }, + OmittedSerialNativeRow { + module: "Canon", + table: "AFInfo2", + serial_index: 9, + variant: false, + alternative: 0, + name: Some("AFAreaHeights"), + reasons: &["serial_emitter_format"], + }, + OmittedSerialNativeRow { + module: "Canon", + table: "AFInfo2", + serial_index: 10, + variant: false, + alternative: 0, + name: Some("AFAreaXPositions"), + reasons: &["serial_emitter_format"], + }, + OmittedSerialNativeRow { + module: "Canon", + table: "AFInfo2", + serial_index: 11, + variant: false, + alternative: 0, + name: Some("AFAreaYPositions"), + reasons: &["serial_emitter_format"], + }, + OmittedSerialNativeRow { + module: "Canon", + table: "AFInfo2", + serial_index: 12, + variant: false, + alternative: 0, + name: Some("AFPointsInFocus"), + reasons: &["serial_decode_bits_words", "serial_emitter_format"], + }, + OmittedSerialNativeRow { + module: "Canon", + table: "AFInfo2", + serial_index: 13, + variant: true, + alternative: 0, + name: Some("AFPointsSelected"), + reasons: &[ + "serial_condition_missing_member", + "serial_decode_bits_words", + "serial_emitter_format", + ], + }, + OmittedSerialNativeRow { + module: "Canon", + table: "AFInfo2", + serial_index: 13, + variant: true, + alternative: 1, + name: Some("Canon_AFInfo2_0x000d"), + reasons: &["serial_emitter_format", "serial_row_shape"], + }, + OmittedSerialNativeRow { + module: "Canon", + table: "AFInfo2", + serial_index: 14, + variant: false, + alternative: 0, + name: Some("PrimaryAFPoint"), + reasons: &["serial_condition_missing_member"], + }, + OmittedSerialNativeRow { + module: "Real", + table: "MediaProps", + serial_index: 1, + variant: false, + alternative: 0, + name: Some("StreamMaxBitrate"), + reasons: &["serial_print_conv_expr"], + }, + OmittedSerialNativeRow { + module: "Real", + table: "MediaProps", + serial_index: 2, + variant: false, + alternative: 0, + name: Some("StreamAvgBitrate"), + reasons: &["serial_print_conv_expr"], + }, + OmittedSerialNativeRow { + module: "Real", + table: "MediaProps", + serial_index: 6, + variant: false, + alternative: 0, + name: Some("StreamPreroll"), + reasons: &["serial_print_conv_expr", "serial_value_conv"], + }, + OmittedSerialNativeRow { + module: "Real", + table: "MediaProps", + serial_index: 7, + variant: false, + alternative: 0, + name: Some("StreamDuration"), + reasons: &["serial_print_conv_expr", "serial_value_conv"], + }, + OmittedSerialNativeRow { + module: "Real", + table: "MediaProps", + serial_index: 11, + variant: false, + alternative: 0, + name: Some("StreamMimeType"), + reasons: &["serial_raw_conv"], + }, + OmittedSerialNativeRow { + module: "Real", + table: "MediaProps", + serial_index: 21, + variant: false, + alternative: 0, + name: Some("FileInfoProperties"), + reasons: &["serial_emitter_format", "serial_row_shape"], + }, + OmittedSerialNativeRow { + module: "Real", + table: "Properties", + serial_index: 0, + variant: false, + alternative: 0, + name: Some("MaxBitrate"), + reasons: &["serial_print_conv_expr"], + }, + OmittedSerialNativeRow { + module: "Real", + table: "Properties", + serial_index: 1, + variant: false, + alternative: 0, + name: Some("AvgBitrate"), + reasons: &["serial_print_conv_expr"], + }, + OmittedSerialNativeRow { + module: "Real", + table: "Properties", + serial_index: 5, + variant: false, + alternative: 0, + name: Some("Duration"), + reasons: &["serial_print_conv_expr", "serial_value_conv"], + }, + OmittedSerialNativeRow { + module: "Real", + table: "Properties", + serial_index: 6, + variant: false, + alternative: 0, + name: Some("Preroll"), + reasons: &["serial_print_conv_expr", "serial_value_conv"], + }, + OmittedSerialNativeRow { + module: "Real", + table: "Properties", + serial_index: 10, + variant: false, + alternative: 0, + name: Some("Flags"), + reasons: &["serial_print_conv"], + }, +]; +pub static OMITTED_SERIAL_NATIVE_TABLES: &[OmittedSerialNativeTable] = &[]; diff --git a/tests/serial_directory_native.rs b/tests/serial_directory_native.rs new file mode 100644 index 000000000..b5f0c51dd --- /dev/null +++ b/tests/serial_directory_native.rs @@ -0,0 +1,513 @@ +//! Native callback replay for the inactive generated `ProcessSerialData` tables. +//! +//! This test intentionally compares only the surface that the native probe +//! observes: `FoundTag` input values, `GetTagInfo` selection/group facts, and +//! bounded `ReadValue` progression through the selected `Real::AudioV3` and +//! `Real::AudioV4` tables. The probe itself records that a `FoundTag` callback +//! does **not** prove final ExifTool key/group reporting, so these assertions +//! do not claim carrier activation or final metadata-group parity. +//! +//! Run explicitly with the canonical native environment: +//! +//! ```text +//! env -u PERL5LIB -u PERLLIB -u PERL5OPT \ +//! EXIFTOOL_PERL=perl \ +//! OXIDEX_PINNED_EXIFTOOL=/path/to/pinned-exiftool \ +//! cargo test --test serial_directory_native -- --ignored +//! ``` + +use std::collections::HashMap; +use std::env; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; + +use oxidex::core::TagValue; +use oxidex::exiftool_oracle::repo_pin; +use oxidex::exiftool_tables::{ + Ctx, Emitted, MemberValue, SerialDir, SerialEmissionSink, SerialTable, find_serial_table, + process_serial_directory, +}; +use oxidex::io::ByteOrder; +use serde_json::{Value, json}; + +const CANONICAL_PERL: &str = "v5.38.2"; +const PROBE: &str = "tools/exiftool-tables/probe_serial_processor.pl"; + +#[derive(Clone, Copy)] +enum Order { + Ii, + Mm, +} + +impl Order { + const fn native(self) -> &'static str { + match self { + Self::Ii => "II", + Self::Mm => "MM", + } + } + + const fn rust(self) -> ByteOrder { + match self { + Self::Ii => ByteOrder::Little, + Self::Mm => ByteOrder::Big, + } + } +} + +#[derive(Default)] +struct Sink { + rows: Vec, + unknown: bool, +} + +impl SerialEmissionSink for Sink { + fn emit(&mut self, row: Emitted) { + self.rows.push(row); + } + + fn serial_enabled(&self, _table: &'static SerialTable) -> bool { + true + } + + fn unknown_enabled(&self) -> bool { + self.unknown + } +} + +struct NativeEnv { + /// Command name or absolute path. CI deliberately supplies `perl`. + perl: String, + source: PathBuf, +} + +struct ReplayCase<'a> { + table: &'static str, + name: &'static str, + order: Order, + data: &'a [u8], + dir_start: usize, + dir_len: usize, + unknown: bool, +} + +impl NativeEnv { + fn require() -> Self { + let perl = env::var("EXIFTOOL_PERL").expect( + "serial native replay was invoked: set EXIFTOOL_PERL to canonical Perl v5.38.2", + ); + assert!( + !perl.is_empty(), + "EXIFTOOL_PERL must name an interpreter command or path" + ); + let source = PathBuf::from(env::var("OXIDEX_PINNED_EXIFTOOL").expect( + "serial native replay was invoked: set OXIDEX_PINNED_EXIFTOOL to ExifTool 13.59 source", + )); + assert!( + source.join("lib/Image/ExifTool/Real.pm").is_file(), + "OXIDEX_PINNED_EXIFTOOL lacks lib/Image/ExifTool/Real.pm: {}", + source.display() + ); + assert!( + source.join("lib/Image/ExifTool/Canon.pm").is_file(), + "OXIDEX_PINNED_EXIFTOOL lacks lib/Image/ExifTool/Canon.pm: {}", + source.display() + ); + let version = Command::new(&perl) + .args(["-e", "print $^V"]) + .output() + .expect("run EXIFTOOL_PERL -e print $^V"); + assert!( + version.status.success(), + "EXIFTOOL_PERL version probe failed" + ); + assert_eq!( + String::from_utf8(version.stdout).expect("Perl version is UTF-8"), + CANONICAL_PERL, + "serial native replay requires canonical Perl {CANONICAL_PERL}" + ); + let exiftool_version = Command::new(&perl) + .args([ + "-Ilib", + "-MImage::ExifTool", + "-e", + "print $Image::ExifTool::VERSION", + ]) + .current_dir(&source) + .env_remove("PERL5LIB") + .env_remove("PERLLIB") + .env_remove("PERL5OPT") + .output() + .expect("load selected Image::ExifTool source"); + assert!( + exiftool_version.status.success(), + "selected Image::ExifTool version probe failed" + ); + assert_eq!( + String::from_utf8(exiftool_version.stdout).expect("ExifTool version is UTF-8"), + repo_pin(), + "serial native replay requires the repository-pinned ExifTool {}", + repo_pin() + ); + Self { perl, source } + } +} + +fn u16(out: &mut Vec, order: Order, value: u16) { + out.extend_from_slice(&match order { + Order::Ii => value.to_le_bytes(), + Order::Mm => value.to_be_bytes(), + }); +} + +fn u32(out: &mut Vec, order: Order, value: u32) { + out.extend_from_slice(&match order { + Order::Ii => value.to_le_bytes(), + Order::Mm => value.to_be_bytes(), + }); +} + +fn real_v3(order: Order, title: &[u8], artist: &[u8], copyright: &[u8], comment: &[u8]) -> Vec { + let mut out = Vec::new(); + u16(&mut out, order, 2); // Channels + for _ in 0..3 { + u16(&mut out, order, 0); // Unknown[3] + } + u16(&mut out, order, 120); // BytesPerMinute + u32(&mut out, order, 1000); // AudioBytes + for value in [title, artist, copyright, comment] { + out.push(u8::try_from(value.len()).expect("fixture length fits u8")); + out.extend_from_slice(value); + } + out +} + +fn real_v4(order: Order) -> Vec { + let mut out = b"RA4!".to_vec(); // FourCC1 undef[4] + u32(&mut out, order, 1000); // AudioFileSize + u16(&mut out, order, 2); // Version2 + u32(&mut out, order, 32); // HeaderSize + u16(&mut out, order, 7); // CodecFlavorID + u32(&mut out, order, 512); // CodedFrameSize + u32(&mut out, order, 9000); // AudioBytes + u32(&mut out, order, 1200); // BytesPerMinute + u32(&mut out, order, 0); // Unknown + u16(&mut out, order, 1); // SubPacketH + u16(&mut out, order, 256); // AudioFrameSize + u16(&mut out, order, 64); // SubPacketSize + u16(&mut out, order, 0); // Unknown + u16(&mut out, order, 44_100); // SampleRate + u16(&mut out, order, 0); // Unknown + u16(&mut out, order, 16); // BitsPerSample + u16(&mut out, order, 2); // Channels + for value in [b"CO2!".as_slice(), b"CO3!".as_slice()] { + out.push(u8::try_from(value.len()).expect("fixture length fits u8")); + out.extend_from_slice(value); + } + out.push(0); // Unknown + u16(&mut out, order, 0); // Unknown + for value in [ + b"Title".as_slice(), + b"Artist".as_slice(), + b"C".as_slice(), + b"Hi".as_slice(), + ] { + out.push(u8::try_from(value.len()).expect("fixture length fits u8")); + out.extend_from_slice(value); + } + out +} + +fn hex(bytes: &[u8]) -> String { + bytes.iter().map(|byte| format!("{byte:02x}")).collect() +} + +fn native_replay(native: &NativeEnv, case: &ReplayCase<'_>) -> Value { + let request = json!({ + "protocol": "oxidex.serial_processor.v1", + "module": "Real", + "table": case.table, + "case": { + "name": case.name, + "byte_order": case.order.native(), + "data_hex": hex(case.data), + "dir_start": case.dir_start, + "dir_len": case.dir_len, + "unknown": if case.unknown { 1 } else { 0 }, + "verbose": 1, + "members": {}, + }, + }); + let probe = Path::new(env!("CARGO_MANIFEST_DIR")).join(PROBE); + assert!( + probe.is_file(), + "serial native probe is missing: {}", + probe.display() + ); + let mut child = Command::new(&native.perl) + .arg(probe) + .args(["--lib", "lib"]) + .current_dir(&native.source) + .env_remove("PERL5LIB") + .env_remove("PERLLIB") + .env_remove("PERL5OPT") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("start native serial probe"); + child + .stdin + .as_mut() + .expect("native probe stdin") + .write_all(format!("{request}\n").as_bytes()) + .expect("write native serial request"); + let output = child + .wait_with_output() + .expect("wait for native serial probe"); + assert!( + output.status.success(), + "native serial probe failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + assert!( + output.stderr.is_empty(), + "native serial probe stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8(output.stdout).expect("native serial JSONL is UTF-8"); + let mut replies = stdout + .lines() + .map(|line| serde_json::from_str::(line).expect("valid native serial JSON")); + let reply = replies.next().expect("one native serial reply"); + assert!( + replies.next().is_none(), + "one request must produce one reply" + ); + assert_eq!( + reply["ok"], + Value::Bool(true), + "native serial probe error: {reply}" + ); + reply +} + +fn rust_replay( + table: &'static SerialTable, + case: &ReplayCase<'_>, +) -> (Vec, oxidex::exiftool_tables::SerialWalkResult) { + let mut sink = Sink { + unknown: case.unknown, + ..Sink::default() + }; + let mut members: HashMap<&'static str, MemberValue> = HashMap::new(); + let result = process_serial_directory( + table, + SerialDir { + data: case.data, + dir_start: case.dir_start, + dir_len: case.dir_len, + base: 0, + data_pos: 0, + byte_order: case.order.rust(), + }, + &mut Ctx::new(&mut members), + &mut sink, + ); + (sink.rows, result) +} + +fn native_rows(reply: &Value) -> Vec<(String, String)> { + reply["found_tags"] + .as_array() + .expect("native found_tags array") + .iter() + .map(|row| { + let name = row["tag_info"]["name"]["string"] + .as_str() + .expect("native tag name") + .to_owned(); + let value = row["value"]["string"] + .as_str() + .expect("V3/V4 callback value is a byte or numeric scalar") + .to_owned(); + (name, value) + }) + .collect() +} + +fn row_value(value: &TagValue) -> String { + value + .as_string() + .map(str::to_owned) + .or_else(|| value.as_integer().map(|value| value.to_string())) + .unwrap_or_else(|| panic!("V3/V4 used subset must emit a string or integer: {value:?}")) +} + +fn rust_rows(rows: &[Emitted]) -> Vec<(String, String)> { + rows.iter() + .map(|row| (row.name.to_owned(), row_value(&row.value))) + .collect() +} + +fn assert_source_groups(table: &'static SerialTable, reply: &Value) { + let groups = &reply["selection"]["table"]["groups"]["values"]; + assert_eq!(groups["0"]["string"].as_str(), Some(table.group0)); + assert_eq!(groups["1"]["string"].as_str(), Some(table.group1)); + assert_eq!(groups["2"]["string"].as_str(), Some(table.group2)); + + // This compares source facts exposed through native GetTagInfo to the + // generated tag facts. It deliberately does not infer final metadata + // groups from the FoundTag callback; the probe labels that unobservable. + for row in reply["get_tag_info"] + .as_array() + .expect("native GetTagInfo array") + { + let Some(values) = row["result"]["groups"]["values"].as_object() else { + continue; + }; + let name = row["result"]["name"]["string"] + .as_str() + .expect("grouped native row name"); + let tag = table + .entries + .iter() + .flat_map(|entry| entry.alternatives) + .find(|tag| tag.name == name) + .unwrap_or_else(|| panic!("generated serial row missing native group fact for {name}")); + for (group, expected) in [ + ("0", tag.groups.g0), + ("1", tag.groups.g1), + ("2", tag.groups.g2), + ] { + match expected { + Some(expected) => assert_eq!( + values[group]["string"].as_str(), + Some(expected), + "{name} group {group}" + ), + None => assert!( + values.get(group).is_none(), + "{name} unexpectedly has group {group}" + ), + } + } + } +} + +fn table(name: &str) -> &'static SerialTable { + let table = + find_serial_table("Real", name).unwrap_or_else(|| panic!("Real::{name} must be generated")); + assert!( + table.gate_a.passes(), + "Real::{name} gate A: {:?}", + table.gate_a.blocked_by + ); + table +} + +fn assert_case(native: &NativeEnv, case: &ReplayCase<'_>) { + let native_reply = native_replay(native, case); + let table = table(case.table); + let (rows, result) = rust_replay(table, case); + assert!( + !result.tainted, + "{}: shared serial reader refused a native V3/V4 subset case: {result:?}", + case.name + ); + assert_eq!( + rust_rows(&rows), + native_rows(&native_reply), + "{}: FoundTag input projection", + case.name + ); + assert_source_groups(table, &native_reply); +} + +#[test] +#[ignore = "requires EXIFTOOL_PERL v5.38.2 and OXIDEX_PINNED_EXIFTOOL; run explicitly"] +fn real_audio_serial_tables_replay_pinned_native_callbacks() { + let native = NativeEnv::require(); + for order in [Order::Ii, Order::Mm] { + let v3 = real_v3(order, b"Hi!", b"Me", b"C", b"Yo"); + assert_case( + &native, + &ReplayCase { + table: "AudioV3", + name: "v3-normal", + order, + data: &v3, + dir_start: 0, + dir_len: v3.len(), + unknown: false, + }, + ); + let v4 = real_v4(order); + assert_case( + &native, + &ReplayCase { + table: "AudioV4", + name: "v4-normal", + order, + data: &v4, + dir_start: 0, + dir_len: v4.len(), + unknown: false, + }, + ); + } + + let normal = real_v3(Order::Ii, b"Hi!", b"Me", b"C", b"Yo"); + for (name, unknown) in [("v3-unknown-off", false), ("v3-unknown-on", true)] { + assert_case( + &native, + &ReplayCase { + table: "AudioV3", + name, + order: Order::Ii, + data: &normal, + dir_start: 0, + dir_len: normal.len(), + unknown, + }, + ); + } + + let zero = real_v3(Order::Ii, b"", b"", b"", b""); + let nul = real_v3(Order::Ii, b"H\0!", b"Me", b"C", b"Yo"); + let truncated = &normal[..15]; // title count remains 3 but no title byte fits. + for (name, data) in [ + ("v3-zero-strings", zero.as_slice()), + ("v3-nul-string", nul.as_slice()), + ("v3-truncated-string", truncated), + ] { + assert_case( + &native, + &ReplayCase { + table: "AudioV3", + name, + order: Order::Ii, + data, + dir_start: 0, + dir_len: data.len(), + unknown: false, + }, + ); + } + + let mut bounded = vec![0xa5, 0x5a, 0xff]; + bounded.extend_from_slice(&normal); + assert_case( + &native, + &ReplayCase { + table: "AudioV3", + name: "v3-bounded-nonzero-offset", + order: Order::Ii, + data: &bounded, + dir_start: 3, + dir_len: normal.len(), + unknown: false, + }, + ); +} diff --git a/tools/exiftool-tables/README.md b/tools/exiftool-tables/README.md index c5f5611a9..72437f5fc 100644 --- a/tools/exiftool-tables/README.md +++ b/tools/exiftool-tables/README.md @@ -8,7 +8,7 @@ the pin remains 13.59. Conservative IFD-aware upgrade classification, one generated-output inventory, verified Canon CODE references and isolated upgrade orchestration are implemented. -The inventory contains 31 outputs, including inactive keyed definitions and the +The inventory contains 32 outputs, including inactive keyed and serial definitions and the [Sony plain producer recovery](../../docs/reference/sony-plain-generator-recovery.md). See the [execution plan](../../docs/UPGRADE-NEXT-STEPS.md) for original validation evidence and the remaining work. The @@ -21,7 +21,7 @@ sides; classifier AUTO/HAND percentages do not measure upgrade coding effort. ## Commands and scope ```sh -just regen-tables # primary generation, including binary, IFD and keyed tables +just regen-tables # primary generation, including binary, IFD, keyed and serial tables just verify-tables # independent checks of committed table declarations just regen-tables-all # primary generation plus all declared downstream tables just regen-tables-tier2 # downstream generation only @@ -33,12 +33,12 @@ scripts refuse a dirty tree unless the explicit override is provided and recorde These direct generation commands modify the caller's artifacts. Use the bump command below when comparing versions in isolated source trees. -`regen.sh` generates binary, IFD and inactive keyed tables, file identification, Composite +`regen.sh` generates binary, IFD and inactive keyed/serial tables, file identification, Composite definitions and compiled Composite expressions, FITS names, and expression/value- conversion ledgers. `regen-all.sh` adds vendor subdirectory tables, Nikon AF-point grids, bespoke transcriptions, recovered Sony/Minolta/Nikon generators, Macintosh CJK charset tables, GeoTIFF key maps, DICOM dictionaries and lens alternatives. -`artifacts.py` is the single output inventory: 9 tier-1 and 22 tier-2 artifacts. +`artifacts.py` is the single output inventory: 10 tier-1 and 22 tier-2 artifacts. Both scripts resolve their output paths and formatting sets from it; the bump's promotion/recovery sets and CI's tier-2 comparison use the same inventory. The bump classifier also @@ -113,7 +113,7 @@ for the restored field, bounded validation and remaining real-file acceptance. ## Generated-output inventory and write checks ```sh -python3 tools/exiftool-tables/artifacts.py paths # all 31 outputs +python3 tools/exiftool-tables/artifacts.py paths # all 32 outputs python3 tools/exiftool-tables/artifacts.py paths --tier 2 # downstream outputs python3 tools/exiftool-tables/artifacts.py paths --tier 1 --kind rust --absolute python3 tools/exiftool-tables/artifacts.py path composite-compute diff --git a/tools/exiftool-tables/artifacts.py b/tools/exiftool-tables/artifacts.py index 4107b02d3..af360a234 100644 --- a/tools/exiftool-tables/artifacts.py +++ b/tools/exiftool-tables/artifacts.py @@ -34,6 +34,7 @@ class Artifact: Artifact("binary", 1, "codegen", "src/exiftool_tables/binary_tables.rs"), Artifact("ifd", 1, "codegen", "src/exiftool_tables/ifd_tables.rs"), Artifact("keyed", 1, "codegen", "src/exiftool_tables/keyed_tables.rs"), + Artifact("serial", 1, "serial_directory", "src/exiftool_tables/serial_tables.rs"), Artifact("expr-ledger", 1, "verify_exprs", "tools/exiftool-tables/expr_oracle_ledger.json"), Artifact("value-ledger", 1, "codegen", "tools/exiftool-tables/value_conv_ledger.json"), Artifact("filetypes", 1, "codegen_filetypes", "src/filetype/tables.rs"), diff --git a/tools/exiftool-tables/regen.sh b/tools/exiftool-tables/regen.sh index 80281ac7c..061206c09 100755 --- a/tools/exiftool-tables/regen.sh +++ b/tools/exiftool-tables/regen.sh @@ -52,6 +52,7 @@ IFD_OUT="$(artifact_path ifd)" # Generate and verify inactive keyed definitions too; publishing their source # facts does not enable a runtime parser route. KEYED_OUT="$(artifact_path keyed)" +SERIAL_OUT="$(artifact_path serial)" JSON="$CACHE/tables-$VERSION.json" EXPR_LEDGER="$(artifact_path expr-ledger)" VALUE_CONV_LEDGER="$(artifact_path value-ledger)" @@ -85,6 +86,11 @@ python3 "$HERE/codegen.py" "$JSON" -o "$OUT" --ifd-out "$IFD_OUT" \ --keyed-out "$KEYED_OUT" \ --expr-ledger "$EXPR_LEDGER" --value-conv-ledger-out "$VALUE_CONV_LEDGER" +echo +echo ">> generating inactive serial-directory facts" +python3 "$HERE/serial_directory.py" "$JSON" \ + --output "$CACHE/serial-$VERSION.json" --rust-output "$SERIAL_OUT" + echo echo ">> extracting file-identification tables" "$PERL" "$HERE/dump_filetypes.pl" "$LIB" > "$CACHE/filetypes-$VERSION.json" @@ -126,4 +132,8 @@ OXIDEX_ALLOW_DIRTY_TREE=1 python3 "$HERE/verify.py" "$OUT" "$LIB" --oracle "$HER --word-processor Image::ExifTool::CanonCustom::ProcessCanonCustom echo -echo ">> done: $OUT, $IFD_OUT and $KEYED_OUT" +echo ">> verifying serial definitions and omission inventory" +python3 "$HERE/verify_serial_directory.py" "$SERIAL_OUT" "$JSON" + +echo +echo ">> done: $OUT, $IFD_OUT, $KEYED_OUT and $SERIAL_OUT" diff --git a/tools/exiftool-tables/serial_directory.py b/tools/exiftool-tables/serial_directory.py index e44da9378..a6e66cb4d 100644 --- a/tools/exiftool-tables/serial_directory.py +++ b/tools/exiftool-tables/serial_directory.py @@ -17,6 +17,7 @@ import json import re +import codegen import conds import serial_directory_facts as facts from serial_processor_grammar import SERIAL_PROCESSOR_V1_TOKENS @@ -24,6 +25,7 @@ DESCRIPTOR_VERSION = 1 PROCESS_SERIAL_DATA = "ProcessSerialData" +_RUNTIME_FORMATS = frozenset({"int8u", "int16u", "int32u", "string", "undef", "binary"}) class SerialDirectoryRefused(ValueError): @@ -282,7 +284,7 @@ def _entry(index, raw, default_format): "refusals": ["serial_row_shape"], "native_refusal": str(error), }) - return {"serial_index": index, "alternatives": compiled} + return {"serial_index": index, "variant": alternatives is not None, "alternatives": compiled} def _table_meta(meta): @@ -528,11 +530,288 @@ def stale_reason(existing, module, table, table_data): return None +def _rust_fmt(format_operand): + """Render a descriptor format as the shared ``Fmt`` literal. + + ``SerialCount`` carries the number of elements. In particular, + ``string[$val{N}]`` is a one-byte-stride string with a prior-raw count, + while a bare field ``string`` has ProcessSerialData's remaining-bytes + rule. These are deliberately distinct from binary-table formatting. + """ + if not isinstance(format_operand, dict): + raise SerialDirectoryRefused("serial emitted Format fact is malformed") + spelling = format_operand.get("format") + count = format_operand.get("count") + if not isinstance(spelling, str) or not isinstance(count, dict): + raise SerialDirectoryRefused("serial emitted Format fact is incomplete") + if spelling == "string": + return "Fmt::RemainderString" if count.get("kind") == "remaining_bytes" else "Fmt::Str(1)" + if spelling in {"undef", "binary"}: + return "Fmt::Undef(1)" + if spelling == "pstring": + return "Fmt::PString" + modeled = codegen.SCALAR_FORMATS.get(spelling) + if modeled is None: + raise SerialDirectoryRefused(f"serial emitted Format is unsupported: {spelling!r}") + return f"Fmt::{modeled[0]}" + + +def _runtime_format_reason(format_operand): + """Name formats which the staged serial reader has not modeled yet.""" + if not isinstance(format_operand, dict) or not isinstance(format_operand.get("format"), str): + return "serial_emitter_format" + return None if format_operand["format"] in _RUNTIME_FORMATS else "serial_emitter_format" + + +def _rust_count(count): + """Render a closed descriptor count operand as ``SerialCount``.""" + if not isinstance(count, dict): + raise SerialDirectoryRefused("serial emitted count fact is malformed") + kind = count.get("kind") + if kind == "fixed" and isinstance(count.get("value"), int) and count["value"] >= 0: + return f"SerialCount::Fixed {{ value: {count['value']} }}" + if kind == "prior_raw_value" and isinstance(count.get("serial_index"), int) and count["serial_index"] >= 0: + return f"SerialCount::PriorRaw {{ serial_index: {count['serial_index']} }}" + if kind == "floor_div_prior_raw_value": + required = ("serial_index", "add", "divisor") + if all(isinstance(count.get(name), int) for name in required) and count["serial_index"] >= 0 and count["add"] >= 0 and count["divisor"] > 0: + return ( + "SerialCount::FloorDivPriorRaw { " + f"serial_index: {count['serial_index']}, add: {count['add']}, divisor: {count['divisor']} }}" + ) + if kind == "remaining_bytes" and set(count) == {"kind"}: + return "SerialCount::RemainingBytes" + raise SerialDirectoryRefused("serial emitted count is outside the closed descriptor grammar") + + +def _rust_flags(flags): + if not isinstance(flags, dict) or any(not isinstance(flags.get(name), bool) for name in ("unknown", "binary", "list")): + raise SerialDirectoryRefused("serial emitted flags are malformed") + return codegen.ifd_flags_literal(flags["unknown"], flags["binary"], flags["list"], False, False, None) + + +def _rust_groups(groups): + if not isinstance(groups, dict): + raise SerialDirectoryRefused("serial emitted Groups fact is malformed") + return codegen.compile_groups_field(groups, Counter()) + + +def _rust_condition(condition): + if not isinstance(condition, dict): + raise SerialDirectoryRefused("serial emitted Condition fact is malformed") + if condition.get("kind") == "always": + return "None" + if condition.get("kind") == "shared_cond" and condition.get("missing_member") == "shared_default": + compiled = condition.get("compiled") + if isinstance(compiled, str) and compiled: + return f"Some({compiled})" + raise SerialDirectoryRefused("serial emitted Condition is outside the shared execution contract") + + +def _alternative_emission_reasons(alternative): + """Return every reason an alternative cannot become a ``SerialTag``. + + Source-descriptor refusals are authoritative. The additional condition + check protects reports produced by an older descriptor that has not yet + classified native missing-member-as-empty semantics. + """ + if not isinstance(alternative, dict): + return ["serial_row_shape"] + reasons = alternative.get("refusals") + if not isinstance(reasons, list) or any(not isinstance(reason, str) or not reason for reason in reasons): + return ["serial_row_shape"] + out = list(reasons) + condition = alternative.get("condition") + if isinstance(condition, dict) and condition.get("missing_member") == "empty_string": + out.append("serial_condition_missing_member") + return sorted(set(out)) + + +def _gate_src(reasons): + if not reasons: + return "&[]" + return "&[" + ", ".join(f'(\"{codegen.rust_str(reason)}\", {count})' for reason, count in reasons) + "]" + + +def _table_default_module(module): + """Return ExifTool's default group module for a captured table owner. + + ``GetTagTable`` matches the first component after ``Image::ExifTool`` in + ``Image::ExifTool::::``. A nested captured module is thus + ``Fixture`` for ``Fixture::Nested``, not the entire owner spelling. + """ + if not isinstance(module, str) or not module: + raise SerialDirectoryRefused("serial table module is unavailable for native group defaults") + first = module.split("::", 1)[0] + if re.fullmatch(r"[A-Za-z_]\w*", first) is None: + raise SerialDirectoryRefused("serial table module is malformed for native group defaults") + return first + + +def _table_group(groups, family, module): + """Resolve a table group with native ``GetTagTable`` defaults. + + ExifTool initializes a loaded table's false group 0 and 1 values from the + table-owning module, and its false group 2 value to ``Other``. The dump + preserves the authored ``GROUPS`` hash before that initialization, so the + staged literal must apply the same rule rather than treating an absent + group 1 as an empty string. + """ + value = groups.get(str(family)) if isinstance(groups, dict) else None + # Perl's ``unless`` regards both the empty string and the string "0" as + # false. _table_meta already authenticates GROUPS values as strings. + if isinstance(value, str) and value not in ("", "0"): + return value + return _table_default_module(module) if family in (0, 1) else "Other" + + +def _serial_tag_literal(alternative): + """Render one source-cleared alternative; callers record every refusal.""" + if not isinstance(alternative, dict) or not isinstance(alternative.get("name"), str) or not alternative["name"]: + raise SerialDirectoryRefused("serial emitted alternative has no Name") + format_operand = alternative.get("format") + fmt = _rust_fmt(format_operand) + count = _rust_count(format_operand.get("count") if isinstance(format_operand, dict) else None) + condition = _rust_condition(alternative.get("condition")) + conversion = alternative.get("conversion") + if conversion != {"kind": "none"}: + raise SerialDirectoryRefused("serial emitted conversion is not modeled") + return ( + "SerialTag { " + f'name: "{codegen.rust_str(alternative["name"])}", ' + f"format: SerialFormat {{ format: {fmt}, count: {count} }}, condition: {condition}, " + f"flags: {_rust_flags(alternative.get('flags'))}, raw_conv: None, omitted: Omitted::NONE, " + f"value_conv: None, print_conv: PrintConv::None, groups: {_rust_groups(alternative.get('groups'))} }}" + ) + + +def serial_rust_source(population): + """Render a source-selected serial inventory as inactive shared literals. + + This consumes descriptors, never table/name allowlists. Every selected + table receives a literal when its descriptor compiled; table Gate A keeps + partially modeled tables non-executable. Every withheld source + alternative is retained in ``OMITTED_SERIAL_NATIVE_ROWS``. + """ + if not isinstance(population, dict) or population.get("version") != DESCRIPTOR_VERSION: + raise SerialDirectoryRefused("serial emitted population version is unsupported") + if population.get("kind") != "native_serial_layout_population" or not isinstance(population.get("tables"), list): + raise SerialDirectoryRefused("serial emitted population shape is unsupported") + + tables, rows, omitted_tables = [], [], [] + for record in sorted(population["tables"], key=lambda item: (item.get("module", ""), item.get("table", ""))): + module, table = record.get("module"), record.get("table") + if not isinstance(module, str) or not module or not isinstance(table, str) or not table: + raise SerialDirectoryRefused("serial emitted table identity is unavailable") + if record.get("outcome") != "descriptor_compiled": + omitted_tables.append((module, table, ["serial_descriptor_refused"])) + continue + descriptor = record.get("descriptor") + if not isinstance(descriptor, dict) or descriptor.get("module") != module or descriptor.get("table") != table: + raise SerialDirectoryRefused("serial emitted descriptor identity disagrees with population") + facts_ = descriptor.get("table_facts") + processor = descriptor.get("processor") + entries = descriptor.get("entries") + gate = descriptor.get("gate_a", {}).get("blocked_by") + if not isinstance(facts_, dict) or not isinstance(processor, dict) or not isinstance(entries, list) or not isinstance(gate, list): + raise SerialDirectoryRefused("serial emitted descriptor is incomplete") + default_format = facts_.get("default_format") + default_operand = {"format": default_format, "count": {"kind": "fixed", "value": 1}} + if _runtime_format_reason(default_operand) is not None: + omitted_tables.append((module, table, ["serial_emitter_default_format"])) + continue + default_fmt = _rust_fmt(default_operand) + processor_name = processor.get("name") + source_file = processor.get("source_file") + source_sha = processor.get("source_sha256") + body_sha = processor.get("source_body_sha256") + if not all(isinstance(value, str) and value for value in (processor_name, source_file, source_sha, body_sha)): + raise SerialDirectoryRefused("serial emitted processor provenance is incomplete") + + gate_counts = Counter() + native_gate_names = set() + for reason, count in gate: + if not isinstance(reason, str) or not reason or not isinstance(count, int) or count < 1: + raise SerialDirectoryRefused("serial emitted Gate A fact is malformed") + gate_counts[reason] += count + native_gate_names.add(reason) + rendered_entries = [] + for entry in sorted(entries, key=lambda item: item.get("serial_index", -1)): + index = entry.get("serial_index") if isinstance(entry, dict) else None + alternatives = entry.get("alternatives") if isinstance(entry, dict) else None + variant = entry.get("variant") if isinstance(entry, dict) else None + if not isinstance(index, int) or index < 0 or not isinstance(alternatives, list) or not isinstance(variant, bool): + raise SerialDirectoryRefused("serial emitted entry is malformed") + rendered_alternatives = [] + for alternative_index, alternative in enumerate(alternatives): + reasons = _alternative_emission_reasons(alternative) + format_reason = _runtime_format_reason( + alternative.get("format") if isinstance(alternative, dict) else None + ) + if format_reason is not None: + reasons = sorted(set(reasons + [format_reason])) + if reasons: + for reason in reasons: + # Newer descriptors include this in Gate A. Count it + # here only for backwards-compatible inputs. + if reason not in native_gate_names: + gate_counts[reason] += 1 + name = alternative.get("name") if isinstance(alternative, dict) else None + name_src = "None" if not isinstance(name, str) else f'Some("{codegen.rust_str(name)}")' + rows.append((module, table, index, variant, alternative_index, name_src, reasons)) + continue + try: + rendered_alternatives.append(_serial_tag_literal(alternative)) + except SerialDirectoryRefused: + reason = "serial_emitter_shape" + gate_counts[reason] += 1 + name = alternative.get("name") if isinstance(alternative, dict) else None + name_src = "None" if not isinstance(name, str) else f'Some("{codegen.rust_str(name)}")' + rows.append((module, table, index, variant, alternative_index, name_src, [reason])) + if rendered_alternatives: + rendered_entries.append( + f"SerialEntry {{ serial_index: {index}, alternatives: &[{', '.join(rendered_alternatives)}] }}" + ) + groups = facts_.get("groups") + tables.append( + "pub static SERIAL_" + re.sub(r"[^A-Za-z0-9]", "_", f"{module}_{table}").upper() + ": SerialTable = SerialTable { " + f'module: "{codegen.rust_str(module)}", table: "{codegen.rust_str(table)}", ' + f'group0: "{codegen.rust_str(_table_group(groups, 0, module))}", ' + f'group1: "{codegen.rust_str(_table_group(groups, 1, module))}", ' + f'group2: "{codegen.rust_str(_table_group(groups, 2, module))}", ' + f"default_format: {default_fmt}, processor: SerialProcessorFacts {{ name: \"{codegen.rust_str(processor_name)}\", source_file: \"{codegen.rust_str(source_file)}\", " + f"source_sha256: \"{codegen.rust_str(source_sha)}\", source_body_sha256: \"{codegen.rust_str(body_sha)}\" }}, " + f"gate_a: GateA {{ blocked_by: {_gate_src(sorted(gate_counts.items()))} }}, entries: &[{', '.join(rendered_entries)}] }};\n" + ) + + index = [" &" + re.search(r"SERIAL_[A-Z0-9_]+", table).group(0) + "," for table in tables] + row_src = [] + for module, table, index_value, variant, alternative, name, reasons in sorted(rows): + reasons_src = ", ".join(f'"{codegen.rust_str(reason)}"' for reason in reasons) + row_src.append( + f' OmittedSerialNativeRow {{ module: "{codegen.rust_str(module)}", table: "{codegen.rust_str(table)}", ' + f"serial_index: {index_value}, variant: {str(variant).lower()}, alternative: {alternative}, name: {name}, reasons: &[{reasons_src}] }}," + ) + table_src = [] + for module, table, reasons in sorted(omitted_tables): + reasons_src = ", ".join(f'"{codegen.rust_str(reason)}"' for reason in reasons) + table_src.append( + f' OmittedSerialNativeTable {{ module: "{codegen.rust_str(module)}", table: "{codegen.rust_str(table)}", reasons: &[{reasons_src}] }},' + ) + prelude = "//! Generated serial-directory facts; no reader or route is activated by this file.\nuse super::*;\n" + return prelude + "".join(tables) + ( + "pub static ALL_SERIAL_TABLES: &[&SerialTable] = &[\n" + "\n".join(index) + "\n];\n" + "pub static OMITTED_SERIAL_NATIVE_ROWS: &[OmittedSerialNativeRow] = &[\n" + "\n".join(row_src) + "\n];\n" + "pub static OMITTED_SERIAL_NATIVE_TABLES: &[OmittedSerialNativeTable] = &[\n" + "\n".join(table_src) + "\n];\n" + ) + + def main(argv=None): """Write the complete selected serial-processor population from one dump.""" parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("source_dump", help="captured dump_tables.pl JSON input") parser.add_argument("--output", required=True, help="destination JSON report") + parser.add_argument("--rust-output", help="optional inactive shared serial Rust facts") args = parser.parse_args(argv) try: document = json.loads(open(args.source_dump, encoding="utf-8").read()) @@ -541,6 +820,9 @@ def main(argv=None): parser.error(str(error)) with open(args.output, "w", encoding="utf-8", newline="\n") as handle: handle.write(population_json(population)) + if args.rust_output: + with open(args.rust_output, "w", encoding="utf-8", newline="\n") as handle: + handle.write(serial_rust_source(population)) if __name__ == "__main__": diff --git a/tools/exiftool-tables/test_artifacts.py b/tools/exiftool-tables/test_artifacts.py index 58dc1af28..5585d505f 100644 --- a/tools/exiftool-tables/test_artifacts.py +++ b/tools/exiftool-tables/test_artifacts.py @@ -13,8 +13,8 @@ class ManifestTests(unittest.TestCase): def test_unique_valid_partition_and_selectors(self): artifacts.validate() all_items = artifacts.select() - self.assertEqual(len(all_items), 31) - self.assertEqual(len(artifacts.select(1)), 9) + self.assertEqual(len(all_items), 32) + self.assertEqual(len(artifacts.select(1)), 10) self.assertEqual(len(artifacts.select(2)), 22) self.assertEqual(set(all_items), set(artifacts.select(1) + artifacts.select(2))) self.assertTrue(all(a.path.endswith('.rs') for a in artifacts.select(kind='rust'))) diff --git a/tools/exiftool-tables/test_regen_shell.py b/tools/exiftool-tables/test_regen_shell.py index 0b2fca414..594b6b9d9 100644 --- a/tools/exiftool-tables/test_regen_shell.py +++ b/tools/exiftool-tables/test_regen_shell.py @@ -70,6 +70,14 @@ def artifact(producer): dump(args[0]);output(flag('-o'),'binary');output(flag('--ifd-out'),'ifd') output(flag('--keyed-out'),'keyed') output(flag('--value-conv-ledger-out'),'value-ledger') + elif name=='serial_directory.py': + dump(args[0]);output(flag('--rust-output'),'serial') + assert flag('--rust-output')==artifact('serial_directory') + output(flag('--output'),'serial-report') + elif name=='verify_serial_directory.py': + assert pathlib.Path(args[0]).resolve()==artifact('serial_directory') + assert pathlib.Path(args[0]).read_text()=='generated explicit-A serial\n' + dump(args[1]) elif name in ('codegen_filetypes.py','codegen_fits.py','gen_sony_main_extra_tables.py','gen_minolta_a100_tables.py','gen_nikon_settings_tables.py','gen_sony_plain_tables.py'): dump(args[0]);output(flag('-o'),name) elif name=='codegen_composite.py': @@ -204,6 +212,11 @@ def test_both_tiers_and_tier2_use_selected_source_and_complete_checks(self): for name in self.extra_leaves(): self.assertEqual(names.count(name), 1, names) self.assertEqual(names.count('verify_exprs.py'), int(full)) + self.assertEqual(names.count('serial_directory.py'), int(full)) + self.assertEqual(names.count('verify_serial_directory.py'), int(full)) + if full: + self.assertLess(names.index('serial_directory.py'), names.index('rustfmt')) + self.assertGreater(names.index('verify_serial_directory.py'), names.index('rustfmt')) self.assertEqual(names.count('rustfmt'), 2 if full else 1) format_calls = [c for c in calls if c['tool'] == 'rustfmt'] for call, tier in zip(format_calls, (1, 2) if full else (2,)): @@ -228,6 +241,15 @@ def test_each_new_producer_or_verifier_failure_survives_exit_guard(self): self.assertIn('regeneration write-set PASS', result.stdout) self.assertNotIn('>> done:', result.stdout) + def test_serial_producer_and_verifier_failures_survive_tier_one_guard(self): + for leaf in ('serial_directory.py', 'verify_serial_directory.py'): + with self.subTest(leaf=leaf): + result, calls = self.run_regeneration(full=True, env={'CONTROL_FAIL': leaf}) + self.assertEqual(result.returncode, 47, result.stdout + result.stderr) + self.assertEqual(calls[-1]['tool'], leaf) + self.assertIn('injected leaf failure: ' + leaf, result.stderr) + self.assertNotIn('>> done:', result.stdout) + def test_undeclared_writes_refuse_on_success_and_on_leaf_failure(self): for fail in (False, True): with self.subTest(fail=fail): diff --git a/tools/exiftool-tables/test_serial_directory.py b/tools/exiftool-tables/test_serial_directory.py index f12695e33..58655239d 100644 --- a/tools/exiftool-tables/test_serial_directory.py +++ b/tools/exiftool-tables/test_serial_directory.py @@ -100,6 +100,7 @@ def test_descriptor_captures_order_raw_count_domain_and_refusal(self): self.assertEqual(descriptor["gate_a"]["blocked_by"], [["serial_decode_bits_words", 1]]) alternative = descriptor["entries"][3]["alternatives"][0] self.assertEqual(alternative["condition"]["missing_member"], "empty_string") + self.assertEqual(alternative["refusals"], []) self.assertEqual(descriptor["entries"][3]["alternatives"][1]["flags"]["unknown"], True) def test_mutated_supported_operands_produce_a_new_descriptor_and_stale_rejects(self): @@ -135,6 +136,47 @@ def test_one_bad_variant_does_not_hide_its_supported_sibling(self): self.assertEqual(alternatives[1]["refusals"], ["serial_row_shape"]) self.assertIn(["serial_row_shape", 1], descriptor["gate_a"]["blocked_by"]) + def test_rust_emitter_uses_descriptor_facts_and_sidecars_every_withheld_row(self): + population = serial_directory.compile_serial_population({"modules": {"Fixture": {"tables": {"Serial": table()}}}}) + source = serial_directory.serial_rust_source(population) + self.assertIn("pub static SERIAL_FIXTURE_SERIAL: SerialTable", source) + self.assertIn('group0: "MakerNotes", group1: "Fixture", group2: "Camera",', source) + self.assertIn('serial_index: 2, variant: false, alternative: 0, name: Some("Words"), reasons: &["serial_decode_bits_words", "serial_emitter_format"]', source) + self.assertIn('gate_a: GateA { blocked_by: &[("serial_condition_missing_member", 2), ("serial_decode_bits_words", 1), ("serial_emitter_format", 2)] }', source) + self.assertIn('serial_index: 3, variant: true, alternative: 0, name: Some("NonEos"), reasons: &["serial_condition_missing_member"]', source) + self.assertIn('serial_index: 3, variant: true, alternative: 1, name: Some("Skipped"), reasons: &["serial_condition_missing_member"]', source) + + def test_rust_emitter_uses_native_false_group_defaults(self): + changed = table() + changed["meta"]["GROUPS"] = {"0": "0", "1": "", "2": "0"} + population = serial_directory.compile_serial_population({"modules": {"Fixture": {"tables": {"Serial": changed}}}}) + source = serial_directory.serial_rust_source(population) + self.assertIn('group0: "Fixture", group1: "Fixture", group2: "Other",', source) + + def test_rust_emitter_uses_first_nested_module_component_for_native_defaults(self): + changed = table() + population = serial_directory.compile_serial_population( + {"modules": {"Fixture::Nested": {"tables": {"Serial": changed}}}} + ) + source = serial_directory.serial_rust_source(population) + self.assertIn('module: "Fixture::Nested", table: "Serial", group0: "MakerNotes", group1: "Fixture", group2: "Camera",', source) + + def test_rust_emitter_keeps_refused_table_in_table_sidecar(self): + malformed = table() + malformed["tags"] = [] + population = serial_directory.compile_serial_population({"modules": {"Fixture": {"tables": {"Malformed": malformed}}}}) + source = serial_directory.serial_rust_source(population) + self.assertNotIn("SERIAL_FIXTURE_MALFORMED", source) + self.assertIn('OmittedSerialNativeTable { module: "Fixture", table: "Malformed", reasons: &["serial_descriptor_refused"] }', source) + + def test_rust_emitter_refuses_a_format_not_supported_by_the_shared_reader(self): + changed = table() + changed["tags"]["0"]["Format"] = "int16s" + population = serial_directory.compile_serial_population({"modules": {"Fixture": {"tables": {"Serial": changed}}}}) + source = serial_directory.serial_rust_source(population) + self.assertIn('serial_index: 0, variant: false, alternative: 0, name: Some("Count"), reasons: &["serial_emitter_format"]', source) + self.assertIn('("serial_emitter_format", 3)', source) + def test_source_order_and_identity_refuse_malformed_processor(self): bad_order = PROCESSOR_BODY.replace("($val{$index} = $val);", "($et->FoundTag($tagInfo, $val));\n ($val{$index} = $val);") with self.assertRaisesRegex(serial_directory.SerialDirectoryRefused, "complete executable grammar"): @@ -228,7 +270,7 @@ def setUp(self): def compile(self, source=None): return serial_directory.compile_serial_inventory("Canon", "AFInfo", source or self.table) - def test_pinned_afinfo_captures_full_order_and_only_decodebits_is_row_blocker(self): + def test_pinned_afinfo_captures_full_order_and_only_decodebits_is_source_row_blocker(self): descriptor = self.compile() self.assertEqual([entry["serial_index"] for entry in descriptor["entries"]], list(range(13))) self.assertEqual(descriptor["gate_a"]["blocked_by"], [["serial_decode_bits_words", 1]]) @@ -241,6 +283,7 @@ def test_pinned_afinfo_captures_full_order_and_only_decodebits_is_row_blocker(se {"kind": "floor_div_prior_raw_value", "serial_index": 0, "add": 15, "divisor": 16}, ) self.assertEqual(descriptor["entries"][11]["alternatives"][0]["condition"]["missing_member"], "empty_string") + self.assertEqual(descriptor["entries"][11]["alternatives"][0]["refusals"], []) def test_committed_descriptor_replays_the_recorded_native_facts(self): report = Path(__file__).resolve().parents[2] / "docs" / "reference" / "serial-layout-inventory-afinfo.json" @@ -294,6 +337,17 @@ def test_population_accounts_for_every_native_serial_selector_and_generalizes_to self.assertTrue(report.is_file(), report) self.assertEqual(report.read_text(), serial_directory.population_json(population)) + def test_real_audio_pair_emits_dynamic_raw_counts_and_no_omissions(self): + source = serial_directory.serial_rust_source(serial_directory.compile_serial_population(self.document)) + self.assertIn("pub static SERIAL_REAL_AUDIOV3: SerialTable", source) + self.assertIn("pub static SERIAL_REAL_AUDIOV4: SerialTable", source) + self.assertIn('name: "Title", format: SerialFormat { format: Fmt::Str(1), count: SerialCount::PriorRaw { serial_index: 4 } }', source) + self.assertIn('name: "Title", format: SerialFormat { format: Fmt::Str(1), count: SerialCount::PriorRaw { serial_index: 23 } }', source) + self.assertIn('name: "Artist", format: SerialFormat { format: Fmt::Str(1), count: SerialCount::PriorRaw { serial_index: 6 } }', source) + self.assertIn('name: "Artist", format: SerialFormat { format: Fmt::Str(1), count: SerialCount::PriorRaw { serial_index: 25 } }', source) + self.assertNotIn('module: "Real", table: "AudioV3", serial_index:', source) + self.assertNotIn('module: "Real", table: "AudioV4", serial_index:', source) + PINNED_SOURCE = os.environ.get("OXIDEX_PINNED_EXIFTOOL") CANONICAL_PERL = os.environ.get("EXIFTOOL_PERL") @@ -305,27 +359,60 @@ def test_population_accounts_for_every_native_serial_selector_and_generalizes_to class CopiedNativeSerialSource(unittest.TestCase): """The compiler consumes a dump of copied Perl, never a hand-built Rust fact.""" - def dump(self, lib): + def dump(self, lib, module="Canon", table="AFInfo"): import subprocess result = subprocess.run( - [CANONICAL_PERL, str(DUMP_TABLES), str(lib), "Canon"], + [CANONICAL_PERL, str(DUMP_TABLES), str(lib), module], check=True, text=True, capture_output=True, ) - return json.loads(result.stdout)["modules"]["Canon"]["tables"]["AFInfo"] + return json.loads(result.stdout)["modules"][module]["tables"][table] - def copied(self, before, after): + def copied(self, before, after, relative_source="Image/ExifTool/Canon.pm"): import shutil from tempfile import TemporaryDirectory tmp = TemporaryDirectory() self.addCleanup(tmp.cleanup) root = Path(tmp.name) / "lib" shutil.copytree(Path(PINNED_SOURCE) / "lib", root) - canon = root / "Image/ExifTool/Canon.pm" - text = canon.read_text() + source = root / relative_source + text = source.read_text() self.assertIn(before, text) - canon.write_text(text.replace(before, after, 1)) + source.write_text(text.replace(before, after, 1)) return root + def test_native_get_tag_table_defaults_omitted_group_one_to_owner_module(self): + native = subprocess.run( + [CANONICAL_PERL, "-I", str(Path(PINNED_SOURCE) / "lib"), "-MJSON::PP", "-MImage::ExifTool", "-e", + 'my $table = Image::ExifTool::GetTagTable("Image::ExifTool::Canon::AFInfo") ' + 'or die "missing native Canon::AFInfo table\\n"; print encode_json($table->{GROUPS}), "\\n";'], + text=True, capture_output=True, check=True, timeout=15, + ) + self.assertEqual(native.stderr, "") + self.assertEqual(json.loads(native.stdout), {"0": "MakerNotes", "1": "Canon", "2": "Camera"}) + + def test_native_get_tag_table_uses_first_component_of_nested_owner_module(self): + from tempfile import TemporaryDirectory + with TemporaryDirectory() as directory: + lib = Path(directory) / "lib" + package = lib / "Image" / "ExifTool" / "Fixture" / "Nested.pm" + package.parent.mkdir(parents=True) + package.write_text( + "package Image::ExifTool::Fixture::Nested;\n" + "use strict;\n" + "our %Main = ( GROUPS => { 0 => 'MakerNotes', 2 => 'Camera' } );\n" + "1;\n", + encoding="utf-8", + ) + native = subprocess.run( + [CANONICAL_PERL, "-I", str(lib), "-I", str(Path(PINNED_SOURCE) / "lib"), + "-MJSON::PP", "-MImage::ExifTool", "-e", + 'my $table = Image::ExifTool::GetTagTable("Image::ExifTool::Fixture::Nested::Main") ' + 'or die "missing native nested table\\n"; print encode_json($table->{GROUPS}), "\\n";'], + text=True, capture_output=True, check=True, timeout=15, + ) + self.assertEqual(native.stderr, "") + self.assertEqual(json.loads(native.stdout), {"0": "MakerNotes", "1": "Fixture", "2": "Camera"}) + def test_source_count_mutation_changes_fresh_descriptor_and_rejects_stale(self): baseline_source = self.dump(Path(PINNED_SOURCE) / "lib") baseline = serial_directory.compile_serial_inventory("Canon", "AFInfo", baseline_source) @@ -344,6 +431,24 @@ def test_source_decodebits_mutation_remains_named_refusal(self): descriptor = serial_directory.compile_serial_inventory("Canon", "AFInfo", changed_source) self.assertEqual(descriptor["gate_a"]["blocked_by"], [["serial_print_conv_expr", 1]]) + def test_real_audio_copied_source_count_mutation_changes_emitted_literal(self): + baseline_table = self.dump(Path(PINNED_SOURCE) / "lib", "Real", "AudioV3") + baseline = serial_directory.compile_serial_population({"modules": {"Real": {"tables": {"AudioV3": baseline_table}}}}) + changed_table = self.dump( + self.copied( + "5 => { Name => 'Title', Format => 'string[$val{4}]' },", + "5 => { Name => 'Title', Format => 'string[$val{6}]' },", + "Image/ExifTool/Real.pm", + ), + "Real", "AudioV3", + ) + changed = serial_directory.compile_serial_population({"modules": {"Real": {"tables": {"AudioV3": changed_table}}}}) + baseline_source = serial_directory.serial_rust_source(baseline) + changed_source = serial_directory.serial_rust_source(changed) + self.assertIn("SerialCount::PriorRaw { serial_index: 4 }", baseline_source) + self.assertIn("SerialCount::PriorRaw { serial_index: 6 }", changed_source) + self.assertNotEqual(baseline_source, changed_source) + def test_source_unknown_option_mutation_cannot_be_faked_by_a_quoted_string(self): changed_source = self.dump(self.copied( "my $unknown = $et->Options(Unknown => 1);", diff --git a/tools/exiftool-tables/test_verify_serial_directory.py b/tools/exiftool-tables/test_verify_serial_directory.py new file mode 100644 index 000000000..00d623b63 --- /dev/null +++ b/tools/exiftool-tables/test_verify_serial_directory.py @@ -0,0 +1,194 @@ +"""Independent mutations for emitted ProcessSerialData facts. + +The compiler is used only to make a realistic Rust input. The verifier under +test parses that text and re-reads the native document without importing the +emitter or its recognizer. +""" + +import copy +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + +import serial_directory +import verify_serial_directory as audit +from test_serial_directory import table + + +PINNED_DUMP = os.environ.get("OXIDEX_TABLES_JSON") + + +def fixture_document(): + return {"modules": {"Fixture": {"tables": {"Serial": table()}}}} + + +def emitted(document): + return serial_directory.serial_rust_source(serial_directory.compile_serial_population(document)) + + +class SerialArtifactVerifier(unittest.TestCase): + def setUp(self): + self.document = fixture_document() + self.source = emitted(self.document) + + def audit_text(self, source): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "serial_tables.rs" + path.write_text(source, encoding="utf-8") + return audit.audit(self.document, audit.parse_artifact(path)) + + def assert_rejected(self, source, fragment): + result = self.audit_text(source) + self.assertFalse(result.ok) + self.assertTrue(any(fragment in problem for problem in result.mismatches), result.mismatches) + + def test_baseline_accounts_for_all_native_alternatives(self): + result = self.audit_text(self.source) + self.assertTrue(result.ok, result.mismatches) + self.assertEqual((result.expected_tables, result.expected_alternatives), (1, 6)) + self.assertEqual((result.emitted_tables, result.emitted_alternatives, result.omitted_alternatives), (1, 2, 4)) + + def test_empty_artifact_cannot_choose_its_own_denominator(self): + empty = """use super::*; +pub static ALL_SERIAL_TABLES: &[&SerialTable] = &[]; +pub static OMITTED_SERIAL_NATIVE_ROWS: &[OmittedSerialNativeRow] = &[]; +pub static OMITTED_SERIAL_NATIVE_TABLES: &[OmittedSerialNativeTable] = &[]; +""" + self.assert_rejected(empty, "selected native table absent") + + def test_cli_rejects_forged_descriptor_refusal_for_every_native_table(self): + forged = """use super::*; +pub static ALL_SERIAL_TABLES: &[&SerialTable] = &[]; +pub static OMITTED_SERIAL_NATIVE_ROWS: &[OmittedSerialNativeRow] = &[]; +pub static OMITTED_SERIAL_NATIVE_TABLES: &[OmittedSerialNativeTable] = &[ + OmittedSerialNativeTable { module: "Fixture", table: "Serial", reasons: &["serial_descriptor_refused"] }, +]; +""" + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + source_dump = root / "native.json" + artifact = root / "serial_tables.rs" + source_dump.write_text(json.dumps(self.document), encoding="utf-8") + artifact.write_text(forged, encoding="utf-8") + run = subprocess.run( + [sys.executable, str(Path(audit.__file__)), str(artifact), str(source_dump)], + text=True, capture_output=True, timeout=15, + ) + self.assertEqual(run.returncode, 1, run.stdout + run.stderr) + self.assertIn("wholly omitted without independent native proof", run.stderr) + + def test_omission_for_nonexistent_native_row_fails(self): + needle = "];\npub static OMITTED_SERIAL_NATIVE_TABLES" + self.assertIn(needle, self.source) + forged = self.source.replace( + needle, + " OmittedSerialNativeRow { module: \"Bogus\", table: \"Nope\", serial_index: 0, variant: false, alternative: 0, name: Some(\"Bogus\"), reasons: &[\"serial_row_shape\"] },\n];\npub static OMITTED_SERIAL_NATIVE_TABLES", + 1, + ) + self.assert_rejected(forged, "has no selected native source alternative") + + def test_removed_emitted_row_fails_against_native_population(self): + start = self.source.index("SerialEntry { serial_index: 0") + end = audit._span(self.source, start, {","}) + self.assert_rejected(self.source[:start] + self.source[end + 1:], "supported native alternative is absent") + + def test_count_groups_and_processor_identity_mutations_fail(self): + changes = ( + ("count: SerialCount::Fixed { value: 1 }", "count: SerialCount::Fixed { value: 2 }", "name/format/count"), + ('group1: "Fixture"', 'group1: "Wrong"', "effective groups"), + ('source_sha256: "' + "a" * 64 + '"', 'source_sha256: "' + "b" * 64 + '"', "processor source identity"), + ('name: Some("Signed")', 'name: Some("Wrong")', "omission name"), + ) + for before, after, message in changes: + with self.subTest(message=message): + self.assertIn(before, self.source) + self.assert_rejected(self.source.replace(before, after, 1), message) + + def test_native_row_mutation_rejects_stale_artifact(self): + changed = copy.deepcopy(self.document) + changed["modules"]["Fixture"]["tables"]["Serial"]["tags"]["0"]["Name"] = "Renamed" + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "serial_tables.rs" + path.write_text(self.source, encoding="utf-8") + result = audit.audit(changed, audit.parse_artifact(path)) + self.assertFalse(result.ok) + self.assertTrue(any("name/format/count" in problem for problem in result.mismatches), result.mismatches) + + def test_nested_module_uses_native_first_component_group_default(self): + document = fixture_document() + nested = document["modules"].pop("Fixture") + document["modules"]["Fixture::Nested"] = nested + source = emitted(document) + self.assertIn('group1: "Fixture"', source) + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "serial_tables.rs" + path.write_text(source, encoding="utf-8") + result = audit.audit(document, audit.parse_artifact(path)) + self.assertTrue(result.ok, result.mismatches) + + def test_native_processor_identity_and_single_variant_mutations_reject_stale_artifact(self): + processor_changed = copy.deepcopy(self.document) + processor_changed["modules"]["Fixture"]["tables"]["Serial"]["meta"]["PROCESS_PROC"]["__deparse"] += "\n" + single_variant = copy.deepcopy(self.document) + direct = single_variant["modules"]["Fixture"]["tables"]["Serial"]["tags"]["0"] + single_variant["modules"]["Fixture"]["tables"]["Serial"]["tags"]["0"] = {"_variants": [direct]} + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "serial_tables.rs" + path.write_text(self.source, encoding="utf-8") + parsed = audit.parse_artifact(path) + processor_result = audit.audit(processor_changed, parsed) + variant_result = audit.audit(single_variant, parsed) + self.assertTrue(any("processor source identity" in problem for problem in processor_result.mismatches), processor_result.mismatches) + self.assertTrue(any("variant identity" in problem for problem in variant_result.mismatches), variant_result.mismatches) + + def test_unknown_artifact_schema_fails_loudly(self): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "serial_tables.rs" + path.write_text(self.source.replace("default_format:", "unexpected:", 1), encoding="utf-8") + with self.assertRaisesRegex(audit.SerialVerificationError, "schema drift"): + audit.parse_artifact(path) + + +@unittest.skipUnless(PINNED_DUMP, "set OXIDEX_TABLES_JSON to replay the full recorded serial population") +class RecordedSerialArtifactVerifier(unittest.TestCase): + def test_fresh_emitted_artifact_matches_full_recorded_population(self): + document = json.loads(Path(PINNED_DUMP).read_text(encoding="utf-8")) + source = emitted(document) + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "serial_tables.rs" + path.write_text(source, encoding="utf-8") + result = audit.audit(document, audit.parse_artifact(path)) + self.assertTrue(result.ok, result.mismatches) + self.assertEqual((result.expected_tables, result.expected_alternatives), (8, 132)) + self.assertEqual((result.emitted_tables, result.emitted_alternatives, result.omitted_alternatives), (8, 106, 26)) + + def test_cli_rejects_recorded_population_forged_as_descriptor_refusals(self): + document = json.loads(Path(PINNED_DUMP).read_text(encoding="utf-8")) + native = audit.native_population(document) + sidecars = "\n".join( + f' OmittedSerialNativeTable {{ module: "{module}", table: "{table}", reasons: &["serial_descriptor_refused"] }},' + for module, table in sorted(native) + ) + forged = """use super::*; +pub static ALL_SERIAL_TABLES: &[&SerialTable] = &[]; +pub static OMITTED_SERIAL_NATIVE_ROWS: &[OmittedSerialNativeRow] = &[]; +pub static OMITTED_SERIAL_NATIVE_TABLES: &[OmittedSerialNativeTable] = &[ +""" + sidecars + "\n];\n" + with tempfile.TemporaryDirectory() as directory: + artifact = Path(directory) / "serial_tables.rs" + artifact.write_text(forged, encoding="utf-8") + run = subprocess.run( + [sys.executable, str(Path(audit.__file__)), str(artifact), PINNED_DUMP], + text=True, capture_output=True, timeout=15, + ) + self.assertEqual(run.returncode, 1, run.stdout + run.stderr) + self.assertEqual(run.stdout.count("\"expected_tables\": 8"), 1) + self.assertEqual(run.stderr.count("wholly omitted without independent native proof"), 8) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/exiftool-tables/verify_serial_directory.py b/tools/exiftool-tables/verify_serial_directory.py new file mode 100644 index 000000000..900c53ef2 --- /dev/null +++ b/tools/exiftool-tables/verify_serial_directory.py @@ -0,0 +1,724 @@ +#!/usr/bin/env python3 +"""Independently audit staged ``ProcessSerialData`` Rust facts. + +This reads the native dump and emitted Rust directly. It deliberately does +not import ``serial_directory`` or its Perl recognizer: source selection comes +from the captured PROCESS_PROC identity, while source/table/row properties are +interpreted here before they are compared with the artifact. Native execution +of the processor remains a separate replay gate. +""" + +from __future__ import annotations + +import argparse +from collections import Counter +from dataclasses import dataclass +import hashlib +import json +from pathlib import Path, PurePosixPath +import re +import sys +from typing import Any + +import serial_directory_facts as facts + + +PROCESSOR_SUFFIX = "::ProcessSerialData" +RUNTIME_FORMATS = frozenset(("int8u", "int16u", "int32u", "string", "undef", "binary")) +ROW_MODELED = frozenset(("Name", "Format", "Condition", "PrintConv", "RawConv", "ValueConv", "SubDirectory", "Groups", "Unknown", "Binary", "List")) +ROW_DOCUMENTARY = frozenset(("Notes", "_shorthand")) +TABLE_MODELED = frozenset(("PROCESS_PROC", "FORMAT", "GROUPS", "VARS")) +TABLE_DOCUMENTARY = frozenset(("NOTES",)) + + +class SerialVerificationError(ValueError): + """The native dump or Rust artifact cannot be independently audited.""" + + +@dataclass(frozen=True, order=True) +class RowKey: + module: str + table: str + index: int + variant: bool + alternative: int + + +@dataclass(frozen=True) +class NativeTable: + module: str + table: str + processor: tuple[str, str, str, str] + groups: tuple[str, str, str] + default_format: str + rows: dict[RowKey, dict[str, Any]] + table_reasons: tuple[str, ...] + + +@dataclass(frozen=True) +class ArtifactTable: + module: str + table: str + groups: tuple[str, str, str] + default_format: str + processor: tuple[str, str, str, str] + gate: tuple[tuple[str, int], ...] + rows: dict[RowKey, dict[str, Any]] + + +@dataclass(frozen=True) +class Artifact: + tables: dict[tuple[str, str], ArtifactTable] + omissions: dict[RowKey, tuple[str | None, tuple[str, ...]]] + omitted_tables: dict[tuple[str, str], tuple[str, ...]] + + +@dataclass(frozen=True) +class Audit: + expected_tables: int + expected_alternatives: int + emitted_tables: int + emitted_alternatives: int + omitted_alternatives: int + mismatches: tuple[str, ...] + + @property + def ok(self) -> bool: + return not self.mismatches + + +_OPEN = {"(": ")", "[": "]", "{": "}"} +_CLOSE = {value: key for key, value in _OPEN.items()} +_STRING = re.compile(r'"((?:[^"\\]|\\.)*)"\Z', re.S) +_INTEGER = re.compile(r"(?:0|[1-9][0-9]*)\Z") +_SIZED = re.compile(r"^([A-Za-z][A-Za-z0-9_]*)\[(.*)\]$", re.S) +_SCALAR = re.compile(r"^[A-Za-z][A-Za-z0-9_]*$") +_PRIOR = re.compile(r"^\$val\{([0-9]+)\}$") +_FLOOR = re.compile(r"^int\(\(\$val\{([0-9]+)\}\+([0-9]+)\)/([1-9][0-9]*)\)$") +_MEMBER_REGEX = re.compile(r"^\$\$self\{([^}]+)\}\s*(=~|!~)\s*/((?:\\.|[^/\\])*)/([a-z]*)$", re.S) +_MEMBER_STRING = re.compile(r"^\$\$self\{([^}]+)\}\s*(eq|ne)\s*'((?:\\.|[^'\\])*)'$", re.S) +_MEMBER_ARROW_STRING = re.compile(r'^\$self->\{([^}]+)\}\s*(eq|ne)\s*"((?:\\.|[^"\\])*)"$', re.S) + + +def _fail(message: str) -> None: + raise SerialVerificationError(message) + + +def _unescape(value: str) -> str: + """Decode the Rust string subset emitted by codegen's literal writer.""" + out, at = [], 0 + simple = {"\\": "\\", '"': '"', "n": "\n", "r": "\r", "t": "\t", "0": "\0", "'": "'"} + while at < len(value): + char = value[at] + if char != "\\": + out.append(char) + at += 1 + continue + if at + 1 >= len(value): + _fail("unterminated Rust string escape") + escaped = value[at + 1] + if escaped in simple: + out.append(simple[escaped]) + at += 2 + elif escaped == "x" and at + 3 < len(value) and re.fullmatch(r"[0-9a-fA-F]{2}", value[at + 2:at + 4]): + out.append(chr(int(value[at + 2:at + 4], 16))) + at += 4 + elif escaped == "u" and at + 2 < len(value) and value[at + 2] == "{": + end = value.find("}", at + 3) + if end < 0 or not re.fullmatch(r"[0-9a-fA-F]{1,6}", value[at + 3:end]): + _fail("invalid Rust unicode escape") + out.append(chr(int(value[at + 3:end], 16))) + at = end + 1 + else: + _fail(f"unknown Rust string escape \\{escaped}") + return "".join(out) + + +def _string(value: str) -> str: + match = _STRING.fullmatch(value.strip()) + if match is None: + _fail("expected Rust string literal") + return _unescape(match.group(1)) + + +def _span(text: str, start: int, terminators: set[str]) -> int: + """Find an expression end, honoring nested delimiters and strings.""" + stack: list[str] = [] + quoted = False + escaped = False + for at in range(start, len(text)): + char = text[at] + if quoted: + if escaped: + escaped = False + elif char == "\\": + escaped = True + elif char == '"': + quoted = False + continue + if char == '"': + quoted = True + elif char in _OPEN: + stack.append(char) + elif char in _CLOSE: + if not stack: + if char in terminators: + return at + _fail("unbalanced Rust expression") + if stack.pop() != _CLOSE[char]: + _fail("mismatched Rust expression delimiter") + elif not stack and char in terminators: + return at + if not stack and "," in terminators: + return len(text) + _fail("unterminated Rust expression") + + +def _split_members(body: str, what: str) -> dict[str, str]: + parts: list[str] = [] + at = 0 + while at < len(body): + end = _span(body, at, {","}) + part = body[at:end].strip() + if part: + parts.append(part) + at = end + 1 + result: dict[str, str] = {} + for part in parts: + match = re.fullmatch(r"([a-z][a-z0-9_]*)\s*:\s*(.+)", part, re.S) + if match is None or match.group(1) in result: + _fail(f"malformed or duplicate {what} member") + result[match.group(1)] = match.group(2).strip() + return result + + +def _struct(value: str, constructor: str, fields: set[str]) -> dict[str, str]: + text = value.strip().removesuffix(",").strip() + match = re.match(re.escape(constructor) + r"\s*\{", text) + if match is None: + _fail(f"expected {constructor} literal") + end = _span(text, match.end(), {"}"}) + if text[end] != "}" or text[end + 1:].strip(): + _fail(f"malformed {constructor} literal") + members = _split_members(text[match.end():end], constructor) + if set(members) != fields: + _fail(f"{constructor} schema drift: {sorted(set(members) ^ fields)}") + return members + + +def _array(value: str, what: str) -> list[str]: + text = value.strip().removesuffix(",").strip() + if not text.startswith("&["): + _fail(f"expected {what} array") + end = _span(text, 2, {"]"}) + if text[end] != "]" or text[end + 1:].strip(): + _fail(f"malformed {what} array") + body = text[2:end] + items: list[str] = [] + at = 0 + while at < len(body): + end = _span(body, at, {","}) + item = body[at:end].strip() + if item: + items.append(item) + at = end + 1 + return items + + +def _fmt(value: str) -> str: + text = re.sub(r"\s+", "", value) + mapping = { + "Fmt::Int8u": "int8u", "Fmt::Int16u": "int16u", "Fmt::Int32u": "int32u", + "Fmt::Str(1)": "string", "Fmt::Undef(1)": "undef", "Fmt::RemainderString": "string", + } + if text not in mapping: + _fail(f"unknown serial Rust format {value!r}") + return mapping[text] + + +def _count(value: str) -> tuple[Any, ...]: + text = re.sub(r"\s+", "", value) + if match := re.fullmatch(r"SerialCount::Fixed\{value:(\d+)\}", text): + return ("fixed", int(match.group(1))) + if match := re.fullmatch(r"SerialCount::PriorRaw\{serial_index:(\d+)\}", text): + return ("prior", int(match.group(1))) + if match := re.fullmatch(r"SerialCount::FloorDivPriorRaw\{serial_index:(\d+),add:(\d+),divisor:(\d+)\}", text): + return ("floor", int(match.group(1)), int(match.group(2)), int(match.group(3))) + if text == "SerialCount::RemainingBytes": + return ("remaining",) + _fail("unknown serial count literal") + + +def _condition(value: str) -> Any: + text = value.strip() + if text == "None": + return None + match = re.fullmatch(r"Some\(\s*Cond::MemberRegex\s*\{(.*)\}\s*\)", text, re.S) + if match: + fields = _split_members(match.group(1), "Cond::MemberRegex") + if set(fields) != {"member", "pattern", "ignore_case", "negate"}: + _fail("serial MemberRegex schema drift") + if fields["ignore_case"] not in {"true", "false"} or fields["negate"] not in {"true", "false"}: + _fail("serial MemberRegex boolean is invalid") + return ("regex", _string(fields["member"]), _string(fields["pattern"]), + fields["ignore_case"] == "true", fields["negate"] == "true") + match = re.fullmatch(r"Some\(\s*Cond::MemberStrEq\s*\{(.*)\}\s*\)", text, re.S) + if match: + fields = _split_members(match.group(1), "Cond::MemberStrEq") + if set(fields) != {"member", "value", "negate"} or fields["negate"] not in {"true", "false"}: + _fail("serial MemberStrEq schema drift") + return ("string", _string(fields["member"]), _string(fields["value"]), fields["negate"] == "true") + _fail("unknown serial condition literal") + + +def _flags(value: str) -> tuple[bool, bool, bool]: + if value.strip() == "IfdFlags::NONE": + return (False, False, False) + fields = _struct(value, "IfdFlags", {"unknown", "binary", "list", "protected", "avoid", "priority"}) + if fields["protected"] != "false" or fields["avoid"] != "false" or fields["priority"] != "None": + _fail("serial artifact uses unsupported IFD flags") + if any(fields[name] not in {"true", "false"} for name in ("unknown", "binary", "list")): + _fail("serial flag boolean is invalid") + return tuple(fields[name] == "true" for name in ("unknown", "binary", "list")) + + +def _groups(value: str) -> tuple[str | None, str | None, str | None]: + if value.strip() == "TagGroups::NONE": + return (None, None, None) + fields = _struct(value, "TagGroups", {"g0", "g1", "g2"}) + out = [] + for name in ("g0", "g1", "g2"): + if fields[name].strip() == "None": + out.append(None) + else: + match = re.fullmatch(r"Some\((.*)\)", fields[name].strip(), re.S) + if match is None: + _fail("serial TagGroups member is invalid") + out.append(_string(match.group(1))) + return tuple(out) + + +def _processor(value: str) -> tuple[str, str, str, str]: + fields = _struct(value, "SerialProcessorFacts", {"name", "source_file", "source_sha256", "source_body_sha256"}) + out = tuple(_string(fields[name]) for name in ("name", "source_file", "source_sha256", "source_body_sha256")) + if not re.fullmatch(r"[0-9a-f]{64}", out[2]) or not re.fullmatch(r"[0-9a-f]{64}", out[3]): + _fail("serial processor digest is invalid") + if (not out[1] or PurePosixPath(out[1]).is_absolute() or "\\" in out[1] + or any(part in {"", ".", ".."} for part in out[1].split("/"))): + _fail("serial processor source file is not library-relative") + return out + + +def _gate(value: str) -> tuple[tuple[str, int], ...]: + fields = _struct(value, "GateA", {"blocked_by"}) + pairs = [] + for item in _array(fields["blocked_by"], "GateA"): + match = re.fullmatch(r"\(\s*(\"(?:[^\"\\]|\\.)*\")\s*,\s*(\d+)\s*\)", item, re.S) + if match is None or int(match.group(2)) == 0: + _fail("serial GateA member is invalid") + pairs.append((_string(match.group(1)), int(match.group(2)))) + if tuple(sorted(pairs)) != tuple(pairs) or len({name for name, _ in pairs}) != len(pairs): + _fail("serial GateA members are not sorted/unique") + return tuple(pairs) + + +def _tag(value: str) -> dict[str, Any]: + fields = _struct(value, "SerialTag", {"name", "format", "condition", "flags", "raw_conv", "omitted", "value_conv", "print_conv", "groups"}) + if fields["raw_conv"].strip() != "None" or fields["omitted"].strip() != "Omitted::NONE" or fields["value_conv"].strip() != "None" or fields["print_conv"].strip() != "PrintConv::None": + _fail("serial artifact uses an unverified conversion/omission projection") + format_fields = _struct(fields["format"], "SerialFormat", {"format", "count"}) + return { + "name": _string(fields["name"]), "format": _fmt(format_fields["format"]), + "count": _count(format_fields["count"]), "condition": _condition(fields["condition"]), + "flags": _flags(fields["flags"]), "groups": _groups(fields["groups"]), + } + + +def _entries(value: str, module: str, table: str) -> dict[RowKey, dict[str, Any]]: + result: dict[RowKey, dict[str, Any]] = {} + for item in _array(value, "SerialEntry"): + fields = _struct(item, "SerialEntry", {"serial_index", "alternatives"}) + if _INTEGER.fullmatch(fields["serial_index"].strip()) is None: + _fail("serial index is invalid") + index = int(fields["serial_index"]) + alternatives = _array(fields["alternatives"], "SerialTag") + if not alternatives: + _fail("serial entry has no alternatives") + for alternative, tag in enumerate(alternatives): + # The schema has no `variant` field for emitted rows. The native + # inventory supplies that identity during audit; use false here and + # resolve it against the unique native alternative at this index. + key = RowKey(module, table, index, False, alternative) + if key in result: + _fail("duplicate serial artifact row") + result[key] = _tag(tag) + return result + + +def _static_value(source: str, name: str) -> str: + marker = re.search(r"pub\s+static\s+" + re.escape(name) + r"\s*:\s*[^=]+?=\s*", source) + if marker is None: + _fail(f"serial artifact lacks {name}") + end = _span(source, marker.end(), {";"}) + if source[end] != ";": + _fail(f"serial artifact {name} is unterminated") + return source[marker.end():end].strip() + + +def parse_artifact(path: Path) -> Artifact: + source = path.read_text(encoding="utf-8") + table_markers = list(re.finditer(r"pub\s+static\s+([A-Za-z_][A-Za-z0-9_]*)\s*:\s*SerialTable\s*=\s*", source)) + tables: dict[tuple[str, str], ArtifactTable] = {} + symbols: dict[str, tuple[str, str]] = {} + for marker in table_markers: + end = _span(source, marker.end(), {";"}) + if source[end] != ";": + _fail("unterminated SerialTable static") + fields = _struct(source[marker.end():end], "SerialTable", {"module", "table", "group0", "group1", "group2", "default_format", "processor", "gate_a", "entries"}) + module, table = _string(fields["module"]), _string(fields["table"]) + key = (module, table) + if key in tables or marker.group(1) in symbols: + _fail("duplicate generated serial table") + tables[key] = ArtifactTable(module, table, + (_string(fields["group0"]), _string(fields["group1"]), _string(fields["group2"])), + _fmt(fields["default_format"]), _processor(fields["processor"]), _gate(fields["gate_a"]), + _entries(fields["entries"], module, table)) + symbols[marker.group(1)] = key + index = _array(_static_value(source, "ALL_SERIAL_TABLES"), "ALL_SERIAL_TABLES") + index_symbols = [] + for item in index: + match = re.fullmatch(r"&([A-Za-z_][A-Za-z0-9_]*)", item) + if match is None: + _fail("ALL_SERIAL_TABLES member is invalid") + index_symbols.append(match.group(1)) + if len(index_symbols) != len(set(index_symbols)) or set(index_symbols) != set(symbols): + _fail("ALL_SERIAL_TABLES does not exactly index generated serial tables") + + omissions: dict[RowKey, tuple[str | None, tuple[str, ...]]] = {} + for item in _array(_static_value(source, "OMITTED_SERIAL_NATIVE_ROWS"), "serial omissions"): + fields = _struct(item, "OmittedSerialNativeRow", {"module", "table", "serial_index", "variant", "alternative", "name", "reasons"}) + if _INTEGER.fullmatch(fields["serial_index"].strip()) is None or _INTEGER.fullmatch(fields["alternative"].strip()) is None: + _fail("serial omission identity is invalid") + if fields["variant"].strip() not in {"true", "false"}: + _fail("serial omission variant is invalid") + raw_name = fields["name"].strip() + name = None + if raw_name != "None": + match = re.fullmatch(r"Some\((.*)\)", raw_name, re.S) + if match is None: + _fail("serial omission name is invalid") + name = _string(match.group(1)) + reasons = tuple(_string(reason) for reason in _array(fields["reasons"], "serial omission reasons")) + if not reasons or tuple(sorted(set(reasons))) != reasons: + _fail("serial omission reasons are empty, duplicate, or unsorted") + key = RowKey(_string(fields["module"]), _string(fields["table"]), int(fields["serial_index"]), fields["variant"].strip() == "true", int(fields["alternative"])) + if key in omissions: + _fail("duplicate serial omission") + omissions[key] = (name, reasons) + + omitted_tables: dict[tuple[str, str], tuple[str, ...]] = {} + for item in _array(_static_value(source, "OMITTED_SERIAL_NATIVE_TABLES"), "serial table omissions"): + fields = _struct(item, "OmittedSerialNativeTable", {"module", "table", "reasons"}) + key = (_string(fields["module"]), _string(fields["table"])) + reasons = tuple(_string(reason) for reason in _array(fields["reasons"], "serial table omission reasons")) + if key in omitted_tables or not reasons or tuple(sorted(set(reasons))) != reasons: + _fail("serial table omission is invalid") + omitted_tables[key] = reasons + return Artifact(tables, omissions, omitted_tables) + + +def _native_truth(value: Any) -> bool: + if value is None: + return False + if isinstance(value, str): + return value not in {"", "0"} + if isinstance(value, (bool, int, float)): + return value != 0 + _fail("serial native flag is not a literal scalar") + + +def _native_groups(raw: Any, module: str) -> tuple[str, str, str]: + if raw is None: + raw = {} + if not isinstance(raw, dict) or any(not isinstance(key, str) or not isinstance(value, str) for key, value in raw.items()): + _fail("serial native GROUPS is malformed") + if set(raw) - {"0", "1", "2"}: + _fail("serial native GROUPS has unsupported index") + first_module = module.split("::", 1)[0] if isinstance(module, str) else "" + if re.fullmatch(r"[A-Za-z_]\w*", first_module) is None: + _fail("serial native module is malformed for GetTagTable group defaults") + # ExifTool's `Image::.*?::([^:]*)` takes the first component after + # Image::ExifTool, rather than the full nested module spelling. + return tuple(raw.get(str(index)) if raw.get(str(index)) not in {None, "", "0"} else (first_module if index < 2 else "Other") for index in range(3)) + + +def _native_format(tag: dict[str, Any], default: str) -> tuple[str, tuple[Any, ...]]: + value = tag.get("Format") + if value is None: + return default, ("fixed", 1) + if not isinstance(value, str): + _fail("serial native Format is not a literal string") + match = _SIZED.fullmatch(value) + if match: + fmt, count = match.groups() + if _SCALAR.fullmatch(fmt) is None: + _fail("serial native sized Format is malformed") + if _INTEGER.fullmatch(count): + return fmt, ("fixed", int(count)) + if prior := _PRIOR.fullmatch(count): + return fmt, ("prior", int(prior.group(1))) + if floor := _FLOOR.fullmatch(count): + return fmt, ("floor", int(floor.group(1)), int(floor.group(2)), int(floor.group(3))) + _fail("serial native Format count is outside verifier grammar") + if value == "string": + return value, ("remaining",) + if _SCALAR.fullmatch(value) is None: + _fail("serial native Format spelling is malformed") + return value, ("fixed", 1) + + +def _native_condition(value: Any) -> tuple[Any, bool]: + if value is None: + return None, False + if not isinstance(value, str) or not value.strip(): + _fail("serial native Condition is malformed") + text = value.strip() + if match := _MEMBER_REGEX.fullmatch(text): + member, op, pattern, flags = match.groups() + if set(flags) - {"i"}: + _fail("serial native Condition regex flags are unsupported") + return ("regex", member, pattern, "i" in flags, op == "!~"), True + if match := _MEMBER_STRING.fullmatch(text): + member, op, string = match.groups() + # Perl single-quoted strings in the accepted subset only escape quote + # and backslash; preserve other backslashes literally. + string = string.replace("\\'", "'").replace("\\\\", "\\") + return ("string", member, string, op == "ne"), False + if match := _MEMBER_ARROW_STRING.fullmatch(text): + member, op, string = match.groups() + return ("string", member, string, op == "ne"), False + # The staged artifact withholds every member-regex condition because the + # shared Cond evaluator treats absent members differently from Perl's + # empty-string regex subject. For such rows we only need to authenticate + # that native fact and its named refusal, including compound expressions. + if re.search(r"\$\$self\{[^}]+\}\s*(?:=~|!~)\s*/", text): + return None, True + _fail("serial native Condition is outside verifier grammar") + + +def _native_reasons(tag: Any, default: str) -> tuple[dict[str, Any] | None, tuple[str, ...]]: + if not isinstance(tag, dict) or not isinstance(tag.get("Name"), str) or not tag["Name"]: + return None, ("serial_row_shape",) + reasons: list[str] = [] + try: + fmt, count = _native_format(tag, default) + condition, missing_empty = _native_condition(tag.get("Condition")) + except SerialVerificationError: + # The emitter sees no descriptor format after a source-row shape + # refusal, and therefore independently withholds the artifact shape + # as well. This preserves the exact source/sidecar distinction rather + # than guessing a partially parsed operand. + return None, ("serial_emitter_format", "serial_row_shape") + if fmt not in RUNTIME_FORMATS: + reasons.append("serial_emitter_format") + if missing_empty: + reasons.append("serial_condition_missing_member") + raw_print = tag.get("PrintConv") + if raw_print is not None: + if isinstance(raw_print, dict) and raw_print.get("kind") == "expr" and isinstance(raw_print.get("expr"), str): + reasons.append("serial_decode_bits_words" if raw_print["expr"] == "Image::ExifTool::DecodeBits($val, undef, 16)" else "serial_print_conv_expr") + else: + reasons.append("serial_print_conv") + if tag.get("RawConv") is not None: + reasons.append("serial_raw_conv") + if tag.get("ValueConv") is not None: + reasons.append("serial_value_conv") + if tag.get("SubDirectory") is not None: + reasons.append("serial_subdirectory") + for name in sorted(tag): + if name not in ROW_MODELED and name not in ROW_DOCUMENTARY: + reasons.append(f"serial_row_property_{name}") + expected = {"name": tag["Name"], "format": fmt, "count": count, "condition": condition, + "flags": tuple(_native_truth(tag.get(name)) for name in ("Unknown", "Binary", "List")), + "groups": _native_row_groups(tag.get("Groups"))} + return expected, tuple(sorted(set(reasons))) + + +def _native_row_groups(raw: Any) -> tuple[str | None, str | None, str | None]: + if raw is None: + return (None, None, None) + if not isinstance(raw, dict) or set(raw) - {"0", "1", "2"} or any(not isinstance(key, str) or not isinstance(value, str) for key, value in raw.items()): + _fail("serial native row Groups is malformed") + return tuple(raw.get(str(index)) for index in range(3)) + + +def native_population(document: dict[str, Any]) -> dict[tuple[str, str], NativeTable]: + if not isinstance(document, dict) or not isinstance(document.get("modules"), dict): + _fail("serial native dump lacks module dictionary") + output: dict[tuple[str, str], NativeTable] = {} + for module, module_data in document["modules"].items(): + if not isinstance(module, str) or not module or not isinstance(module_data, dict) or not isinstance(module_data.get("tables"), dict): + _fail("serial native module shape is malformed") + for table, table_data in module_data["tables"].items(): + if not isinstance(table, str) or not table or not isinstance(table_data, dict): + _fail("serial native table shape is malformed") + meta = table_data.get("meta") + if not isinstance(meta, dict): + continue + processor = meta.get("PROCESS_PROC") + if not isinstance(processor, dict) or not isinstance(processor.get("__name"), str) or not processor["__name"].endswith(PROCESSOR_SUFFIX): + continue + try: + identity = facts.source_identity(processor) + body_sha = facts.deparse_sha256(processor.get("__deparse")) + except facts.SerialFactRefused as error: + _fail(str(error)) + default = meta.get("FORMAT", "int8u") + if not isinstance(default, str) or _SCALAR.fullmatch(default) is None: + _fail("serial native table FORMAT is malformed") + table_reasons = [] + for name in sorted(meta): + if name in TABLE_MODELED or name in TABLE_DOCUMENTARY: + continue + table_reasons.append("serial_table_priority" if name == "PRIORITY" else f"serial_table_property_{name}") + tags = table_data.get("tags") + if not isinstance(tags, dict): + _fail("serial native selected table has no tag dictionary") + rows: dict[RowKey, dict[str, Any]] = {} + for raw_index, raw in tags.items(): + if not isinstance(raw_index, str) or _INTEGER.fullmatch(raw_index) is None: + _fail("serial native selected table has non-numeric tag key") + index = int(raw_index) + variants = raw.get("_variants") if isinstance(raw, dict) and "_variants" in raw else None + values = variants if variants is not None else [raw] + if not isinstance(values, list) or not values: + _fail("serial native alternative shape is malformed") + for alternative, tag in enumerate(values): + key = RowKey(module, table, index, variants is not None, alternative) + if key in rows: + _fail("duplicate serial native alternative") + expected, reasons = _native_reasons(tag, default) + rows[key] = {"expected": expected, "reasons": reasons, + "name": tag.get("Name") if isinstance(tag, dict) and isinstance(tag.get("Name"), str) else None} + key = (module, table) + if key in output: + _fail("duplicate selected serial native table") + output[key] = NativeTable(module, table, (identity["name"], identity["source_file"], identity["source_sha256"], body_sha), + _native_groups(meta.get("GROUPS"), module), default, rows, tuple(sorted(set(table_reasons)))) + return output + + +def audit(document: dict[str, Any], artifact: Artifact) -> Audit: + native = native_population(document) + mismatches: list[str] = [] + expected_rows = sum(len(table.rows) for table in native.values()) + emitted_rows = sum(len(table.rows) for table in artifact.tables.values()) + native_row_keys = {row_key for table in native.values() for row_key in table.rows} + for row_key in sorted(set(artifact.omissions) - native_row_keys): + mismatches.append(f"{row_key}: generated omission has no selected native source alternative") + for key in sorted(set(native) - set(artifact.tables) - set(artifact.omitted_tables)): + mismatches.append(f"{key}: selected native table absent from artifact and table omissions") + for key in sorted((set(artifact.tables) | set(artifact.omitted_tables)) - set(native)): + mismatches.append(f"{key}: artifact table has no selected native processor") + for key in sorted(set(artifact.tables) & set(artifact.omitted_tables)): + mismatches.append(f"{key}: table appears as both emitted and omitted") + # `serial_descriptor_refused` is an emitter-side report, not independent + # proof about a native table. Accepting it here would let an artifact put + # every selected table in this sidecar and evade all row/provenance checks. + # A later verifier may add a native, separately implemented refusal rule; + # until then a selected table must be emitted and audited. + for key in sorted(set(native) & set(artifact.omitted_tables)): + mismatches.append(f"{key}: selected native table is wholly omitted without independent native proof") + for key, native_table in sorted(native.items()): + if key in artifact.omitted_tables: + continue + emitted = artifact.tables.get(key) + if emitted is None: + continue + if emitted.groups != native_table.groups: + mismatches.append(f"{key}: effective groups differ from native GetTagTable defaults") + if emitted.default_format != native_table.default_format: + mismatches.append(f"{key}: default format differs from native source") + if emitted.processor != native_table.processor: + mismatches.append(f"{key}: processor source identity differs") + expected_gate = Counter(native_table.table_reasons) + for row in native_table.rows.values(): + for reason in row["reasons"]: + expected_gate[reason] += 1 + if emitted.gate != tuple(sorted(expected_gate.items())): + mismatches.append(f"{key}: Gate A reasons/counts differ from native source and omissions") + + native_by_index: dict[tuple[int, int], tuple[RowKey, dict[str, Any]]] = {(row_key.index, row_key.alternative): (row_key, row) for row_key, row in native_table.rows.items()} + artifact_by_index = {(row_key.index, row_key.alternative): row for row_key, row in emitted.rows.items()} + for pair, (native_key, source_row) in sorted(native_by_index.items()): + expected, reasons = source_row["expected"], source_row["reasons"] + omission = artifact.omissions.get(native_key) + emitted_row = artifact_by_index.get(pair) + if reasons: + if emitted_row is not None: + mismatches.append(f"{native_key}: refused native alternative was emitted") + if omission is None or omission[0] != source_row["name"]: + mismatches.append(f"{native_key}: omission name differs from native source") + if omission is None or omission[1] != reasons: + mismatches.append(f"{native_key}: omission reasons differ from native source") + continue + if omission is not None: + mismatches.append(f"{native_key}: supported native alternative was omitted") + continue + if emitted_row is None: + mismatches.append(f"{native_key}: supported native alternative is absent") + continue + if emitted_row != expected: + mismatches.append(f"{native_key}: emitted name/format/count/condition/flags/groups differ from native source") + # A one-alternative native `_variants` array is not distinguishable + # from a direct scalar entry in the current SerialEntry schema. + # Do not silently certify that source-identity change until the + # artifact carries an emitted variant discriminator. + if native_key.variant and sum(1 for candidate in native_table.rows if candidate.index == native_key.index) == 1: + mismatches.append(f"{native_key}: emitted single variant lacks artifact variant identity") + for pair in sorted(set(artifact_by_index) - set(native_by_index)): + mismatches.append(f"{key + pair}: generated serial alternative has no native source row") + expected_omissions = {row_key for row_key, row in native_table.rows.items() if row["reasons"]} + actual_omissions = {row_key for row_key in artifact.omissions if row_key.module == key[0] and row_key.table == key[1]} + for row_key in sorted(actual_omissions - expected_omissions): + mismatches.append(f"{row_key}: generated omission has no refused native source alternative") + return Audit(len(native), expected_rows, len(artifact.tables), emitted_rows, len(artifact.omissions), tuple(mismatches)) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("artifact", type=Path, help="generated serial_tables.rs") + parser.add_argument("source_dump", type=Path, help="fresh native dump_tables.pl JSON") + parser.add_argument("--json-out", type=Path, help="optional machine-readable audit result") + args = parser.parse_args(argv) + try: + document = json.loads(args.source_dump.read_text(encoding="utf-8")) + artifact = parse_artifact(args.artifact) + result = audit(document, artifact) + except (OSError, json.JSONDecodeError, SerialVerificationError) as error: + parser.error(str(error)) + instrument = { + "tool": Path(__file__).name, + "source_dump": str(args.source_dump), + "source_sha256": hashlib.sha256(args.source_dump.read_bytes()).hexdigest(), + "artifact": str(args.artifact), + "artifact_sha256": hashlib.sha256(args.artifact.read_bytes()).hexdigest(), + "scope": "recorded native source and emitted Rust facts; native processor execution is separate", + } + print("=== instrument: verify_serial_directory.py ===", file=sys.stderr) + print(json.dumps(instrument, sort_keys=True), file=sys.stderr) + payload = {"expected_tables": result.expected_tables, "expected_alternatives": result.expected_alternatives, + "emitted_tables": result.emitted_tables, "emitted_alternatives": result.emitted_alternatives, + "omitted_alternatives": result.omitted_alternatives, "mismatches": list(result.mismatches)} + if args.json_out: + args.json_out.write_text(json.dumps(payload, sort_keys=True, indent=2) + "\n", encoding="utf-8") + print(json.dumps(payload, sort_keys=True)) + if result.mismatches: + for mismatch in result.mismatches: + print(f"serial audit: {mismatch}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())