use std::collections::HashMap;
use std::path::PathBuf;
use exiftool_rs::tags::conv_expr::{eval_composite, eval_with, ParseState, Val};
#[derive(Default)]
struct Probe {
asked: std::cell::Cell<bool>,
}
impl ParseState for Probe {
fn member(&self, _: &str) -> Option<Val> {
self.asked.set(true);
Some(Val::Num(1.0))
}
fn option(&self, _: &str) -> Option<Val> {
self.asked.set(true);
Some(Val::Undef)
}
fn byte_order(&self) -> Option<&str> {
self.asked.set(true);
Some("II")
}
fn tag_value(&self, name: &str) -> Option<Val> {
self.asked.set(true);
match name {
"HandlerType" => Some(Val::Str("vide".to_string())),
"MatrixStructure" => Some(Val::Str("1 0 0 0 1 0 0 0 1".to_string())),
_ => None,
}
}
fn tag_group1(&self, _: &str) -> Option<String> {
self.asked.set(true);
Some("Track1".to_string())
}
fn tag_extra(&self, _: &str, _: &str) -> Option<Val> {
self.asked.set(true);
Some(Val::List(vec![Val::Str("m".to_string())]))
}
fn current_tag(&self) -> Option<String> {
self.asked.set(true);
Some("ProbeTag".to_string())
}
}
fn perl_compiles(expr: &str) -> Option<bool> {
Some(perl_verdict(expr)?.is_none())
}
fn perl_verdict(expr: &str) -> Option<Option<String>> {
let out = std::process::Command::new("perl")
.arg("-e")
.arg(
"use strict; use warnings; my ($val, $self, $tag) = (42, undef, 'T'); \
my (@val, @prt, @raw) = ((1,2), (1,2), (1,2)); \
eval $ARGV[0]; \
print $@ if $@ and $@ =~ /syntax error|not terminated|requires explicit package/",
)
.arg(expr)
.output()
.ok()?;
let err = String::from_utf8_lossy(&out.stdout).trim().to_string();
Some(if err.is_empty() {
None
} else {
Some(err.lines().next().unwrap_or_default().trim().to_string())
})
}
fn collect(lib: &PathBuf) -> (Vec<(usize, String)>, HashMap<String, Vec<String>>) {
let mut counts: HashMap<String, usize> = HashMap::new();
let mut sites: HashMap<String, Vec<String>> = HashMap::new();
let dir = lib.join("Image/ExifTool");
let Ok(entries) = std::fs::read_dir(&dir) else {
eprintln!("cannot read {}", dir.display());
std::process::exit(2);
};
for e in entries.flatten() {
let p = e.path();
if p.extension().is_none_or(|x| x != "pm") {
continue;
}
let Ok(text) = std::fs::read_to_string(&p) else {
continue;
};
let b: Vec<char> = text.chars().collect();
let mut i = 0usize;
while i < b.len() {
let Some(key) = ["PrintConv", "ValueConv"]
.into_iter()
.find(|k| b[i..].starts_with(&k.chars().collect::<Vec<_>>()[..]))
else {
i += 1;
continue;
};
i += key.len();
if b[i..].starts_with(&['I', 'n', 'v']) {
continue;
}
while i < b.len() && b[i].is_whitespace() {
i += 1;
}
if !b[i..].starts_with(&['=', '>']) {
continue;
}
i += 2;
while i < b.len() && b[i].is_whitespace() {
i += 1;
}
if b.get(i) != Some(&'\'') {
continue;
}
let start = i;
i += 1;
let mut body = String::new();
while i < b.len() {
if b[i] == '\\' && i + 1 < b.len() {
body.push(b[i]);
body.push(b[i + 1]);
i += 2;
continue;
}
if b[i] == '\'' {
i += 1;
break;
}
body.push(b[i]);
i += 1;
}
let body = body.replace("\\'", "'").replace("\\\\", "\\");
let line = 1 + b[..start].iter().filter(|c| **c == '\n').count();
sites.entry(body.clone()).or_default().push(format!(
"{}:{line}",
p.file_name()
.map_or_else(String::new, |n| n.to_string_lossy().into_owned())
));
*counts.entry(body).or_default() += 1;
}
}
let mut v: Vec<(usize, String)> = counts.into_iter().map(|(e, n)| (n, e)).collect();
v.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)));
(v, sites)
}
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
let check = args.iter().any(|a| a == "--check");
let lib = args.iter().find(|a| !a.starts_with("--")).map_or_else(
|| PathBuf::from("/home/sylvain/dev/exiftool/lib"),
PathBuf::from,
);
let (exprs, sites) = collect(&lib);
let (mut ok_d, mut ok_o, mut no_d, mut no_o) = (0usize, 0usize, 0usize, 0usize);
let (mut state_d, mut state_o) = (0usize, 0usize);
let mut misses: Vec<(usize, String)> = Vec::new();
for (n, e) in &exprs {
let probe = Probe::default();
let composite = ["$val[", "@val", "$prt[", "@prt", "$raw[", "@raw"]
.iter()
.any(|m| e.contains(m));
let parts: Vec<Val> = (1..=12).map(|k| Val::Num(f64::from(k))).collect();
let done = if composite {
eval_composite(e, &parts, &parts, &parts, &probe).is_some()
} else {
eval_with(e, &Val::Num(3.0), &probe).is_some()
|| eval_with(e, &Val::Str("1 2 3 4 5 6 7 8".to_string()), &probe).is_some()
};
if done {
if probe.asked.get() {
state_d += 1;
state_o += n;
}
ok_d += 1;
ok_o += n;
} else {
no_d += 1;
no_o += n;
misses.push((*n, e.clone()));
}
}
let mut broken_o = 0usize;
let mut broken: Vec<(usize, String)> = Vec::new();
let mut perl_missing = false;
misses.retain(|(n, e)| match perl_compiles(e) {
Some(false) => {
broken_o += n;
broken.push((*n, e.clone()));
false
}
Some(true) => true,
None => {
perl_missing = true;
true
}
});
let total_o = ok_o + no_o;
let total_d = ok_d + no_d;
println!("COUNTER ONE — conversion expressions understood by tags::conv_expr");
println!(
" occurrences : {ok_o} / {total_o} ({}%)",
100 * ok_o / total_o.max(1)
);
println!(" distinct : {ok_d} / {total_d}");
let evaluable = total_o - broken_o;
println!(
" of the evaluable: {ok_o} / {evaluable} ({}%)",
100 * ok_o / evaluable.max(1)
);
println!(" state-fed : {state_o} occurrences ({state_d} distinct) read a parse-state");
println!(" member, and are right only once the reader tracks it");
if !broken.is_empty() {
println!(
" unevaluable: {broken_o} occurrences ({} distinct) are not valid Perl -- ExifTool's",
broken.len()
);
println!(" own eval dies on them ($value = eval $conv, ExifTool.pm:3663),");
println!(" leaving no value for any evaluator to produce:");
for (n, e) in &broken {
let shown: String = e.chars().take(72).collect();
let where_ = sites.get(e).map_or_else(String::new, |v| v.join(", "));
println!(" {n:5} {shown}");
println!(" in {where_}");
if let Some(Some(why)) = perl_verdict(e) {
println!(" perl: {why}");
}
}
}
if perl_missing {
println!(" (no perl on PATH: refusals could not be checked against it)");
}
println!();
println!(" still refused, by how often ExifTool uses it:");
for (n, e) in misses.iter().take(80) {
let shown: String = e.chars().take(88).collect();
println!(" {n:5} {shown}");
}
if misses.len() > 80 {
println!(" … and {} more distinct expressions", misses.len() - 80);
}
if check {
println!();
if ok_o == total_o && ok_d == total_d {
println!(
"COUNTER ONE OK — {ok_o} / {total_o} occurrences, {ok_d} / {total_d} distinct."
);
} else {
println!(
"COUNTER ONE FAILED — {ok_o} / {total_o} occurrences, {ok_d} / {total_d} distinct; \
{} occurrence(s) in {} expression(s) are not accounted for.",
total_o - ok_o,
total_d - ok_d
);
std::process::exit(1);
}
}
}