//! The JPEG AI exact decoder's executable imports no C math-library function, and //! defines none but the exactly specified ones. //! //! [`wai::jpegai_decode::decode`] is meant to give the same bytes on every platform //! because its float stages use only IEEE-754 basic operations, with `exp` and rounding //! built from them in `wai::float_repro`. A math-library function on that path (`exp`, //! `log`, `pow`, `fmod`, ...) would tie the output to whichever library a platform //! links. The pinned-output tests need not notice: the decoder rounds its float stages, //! and a last-bit difference can vanish in the rounding on the platforms measured. //! //! This test checks the linked executable rather than the source, so the way the call //! is spelled does not matter: a method call, `f64::exp(x)`, a function path, a float //! `%` (which compiles to `fmod`), or a call made in another module or crate all leave a //! symbol. It is a program (`harness = false` in Cargo.toml, so none of the test //! harness's own code is in the executable) that takes the addresses of the exact //! decoder's public entry points and of the loaders for its weights. The linker keeps //! those and everything they reach: rustc links every executable with unreferenced //! sections removed (`--gc-sections`, `-dead_strip`). The program then lists its own //! executable's symbols with `nm` and fails on: //! - any C math-library function the executable imports, which is a call into the //! platform's math library; //! - any it defines, as Rust's compiler builtins do for some functions on x86_64 Linux, //! except the round-to-integral functions (`ceil`, `floor`, `trunc`, `round`, //! `roundeven`, `rint`, `nearbyint`, and their `f`/`l` forms). //! //! A definition shows that the linker kept a function, not that anything calls it. On //! x86_64 Linux, GNU ld (2.40, debug build) kept the compiler builtins' `floor` in this //! executable although nothing in it referred to `floor`: the only reference had been //! in a section the linker removed as unused, from code the decoder does not reach. In //! a minimal program, GNU ld kept such an unused definition only when the C math library //! was also linked, which other code in the crate's object files can cause. The same //! build linked with LLD held no `floor`. So a definition of a function whose result is //! the same whatever computes it does not fail the test: IEEE 754 fixes the //! round-to-integral results bit for bit (`rint` and `nearbyint` in the current rounding //! direction, which the decoder checks is the default). The program prints the ones it //! allows. Every other math function, defined or imported, still fails. //! //! Also allowed: `sqrt`, `fabs` and `copysign` and their `f`/`l` forms, defined or //! imported. IEEE 754 fixes their results bit for bit, like the other basic operations, //! so the decode may use them however the target compiles them. //! //! Two controls keep an incomplete listing from passing. The executable must show the //! decoder's own `jpegai_decode::decode` symbol, so the decode was linked and the //! listing includes the executable's own symbols; and it must import `malloc` (Rust's //! allocator), so the listing includes imports. //! //! Scope: //! - Unix targets, with `nm` on the PATH (it ships with the linker toolchain these //! targets link with). On other targets (Windows, wasm32) the program prints //! `SKIPPED` and checks nothing. //! - It sees calls, not instructions. A rounding the target does with an instruction //! (AArch64 `frinta`) leaves no symbol. `exp` and the other transcendentals have no //! instruction on these targets and always leave one. //! - It does not enforce, where the compiler builtins define the rounding functions //! (x86_64 Linux), that the decoder's roundings come from `float_repro`; clippy does //! that in the decode-path modules. A rounding call that slips past it cannot change //! the output, for the reason above. //! - It checks the build it is part of (the profile and features `cargo test` was run //! with). CI runs it in the debug profile. //! - `clippy.toml` (`disallowed-methods`) is the source-level guard for the same rule. //! It names the offending line, but sees only method calls, in the modules that deny //! the lint. use std::hint::black_box; use wai::jpegai_decode::{self, DecoderWeights, NormTables, Refusal, Rgb, Stage}; use wai::jpegai_icci::IcciNet; use wai::jpegai_synthesis::{IntNet, Weights}; type Decode = fn(&[u8], &DecoderWeights, &NormTables) -> Result; type DecodeWithStages = fn(&[u8], &DecoderWeights, &NormTables, &mut dyn FnMut(Stage, &[&[f32]])) -> Result; type Check = fn(&[u8]) -> Result<(), &'static str>; /// Take the address of every entry point a sink uses to decode, through `black_box`, /// so the compiler must emit them and the linker must keep them and what they call. /// Nothing is run. fn link_the_decoder() { black_box(jpegai_decode::decode as Decode); black_box(jpegai_decode::decode_with_stages as DecodeWithStages); black_box(jpegai_decode::check_scope as Check); black_box(jpegai_decode::check_codestream as Check); black_box(jpegai_decode::fp_env_is_default as fn() -> bool); black_box(Weights::from_json as fn(&serde_json::Value) -> Result); black_box(IntNet::from_json as fn(&serde_json::Value) -> Result); black_box(IcciNet::from_json as fn(&serde_json::Value) -> Result); } /// C99 `` functions, besides the allowed ones. Each is also checked with the /// `f` and `l` suffixes. const MATH_FUNCTIONS: [&str; 66] = [ "acos", "acosh", "asin", "asinh", "atan", "atan2", "atanh", "cbrt", "ceil", "cos", "cosh", "erf", "erfc", "exp", "exp10", "exp2", "expm1", "fdim", "floor", "fma", "fmax", "fmin", "fmod", "frexp", "gamma", "hypot", "ilogb", "j0", "j1", "jn", "ldexp", "lgamma", "llrint", "llround", "log", "log10", "log1p", "log2", "logb", "lrint", "lround", "modf", "nearbyint", "nextafter", "nexttoward", "pow", "pow10", "remainder", "remquo", "rint", "round", "roundeven", "scalb", "scalbln", "scalbn", "sin", "sincos", "sinh", "tan", "tanh", "tgamma", "trunc", "y0", "y1", "yn", "significand", ]; /// Other names under which a math function reaches an executable: the integer-power /// helpers from the compiler's runtime library (`powi`), and platform variants. const OTHER_MATH_SYMBOLS: [&str; 8] = [ "__powisf2", "__powidf2", "__powitf2", "__sincos_stret", "__sincosf_stret", "__exp10", "__exp10f", "__sinpi", ]; /// A symbol name as `nm` prints it, reduced to the C name: without the version /// (`exp@GLIBC_2.29`, `exp@@GLIBC_2.29`) and, on targets whose vendor is `apple`, without the leading /// underscore Mach-O adds to C names. fn c_name(sym: &str) -> &str { let s = sym.split('@').next().unwrap_or(sym); if cfg!(target_vendor = "apple") { s.strip_prefix('_').unwrap_or(s) } else { s } } /// True if `name` (a C name) is a math-library function this test checks. fn is_math_function(name: &str) -> bool { let base = name.strip_suffix("_finite").map(|b| b.strip_prefix("__").unwrap_or(b)).unwrap_or(name); OTHER_MATH_SYMBOLS.contains(&name) || MATH_FUNCTIONS.iter().any(|f| { base == *f || base.strip_suffix('f') == Some(f) || base.strip_suffix('l') == Some(f) }) } /// The round-to-integral functions, which IEEE 754 fixes bit for bit. A definition of /// one of these is allowed (see the module doc); an import is not. const ROUND_TO_INTEGRAL: [&str; 7] = ["ceil", "floor", "trunc", "round", "roundeven", "rint", "nearbyint"]; fn is_round_to_integral(name: &str) -> bool { ROUND_TO_INTEGRAL.iter().any(|f| { name == *f || name.strip_suffix('f') == Some(f) || name.strip_suffix('l') == Some(f) }) } /// `nm -P` type letters for a symbol the executable does not define: `U`, and the /// lower-case weak forms. fn is_import(kind: &str) -> bool { matches!(kind, "U" | "w" | "v") } /// True if the symbol `sym` of `nm` type `kind` fails the test. fn forbidden(sym: &str, kind: &str) -> bool { let name = c_name(sym); is_math_function(name) && (is_import(kind) || !is_round_to_integral(name)) } /// The classifier recognises the forms `nm` prints, and lets the allowed and unrelated /// names through, before it is trusted with the executable. fn check_the_classifier() { let apple = cfg!(target_vendor = "apple"); let c = |s: &'static str| -> String { if apple { format!("_{s}") } else { s.to_string() } }; let exp = if apple { "_exp".to_string() } else { "exp@GLIBC_2.29".to_string() }; for (sym, kind, want) in [ (exp, "U", true), (if apple { c("fmodf") } else { "fmodf@@GLIBC_2.38".to_string() }, "U", true), (c("fmod"), "W", true), (c("exp"), "T", true), (c("roundf"), "U", true), (if apple { c("floor") } else { "floor@GLIBC_2.2.5".to_string() }, "U", true), (c("floor"), "w", true), (c("floor"), "W", false), (c("roundf"), "W", false), (c("rint"), "T", false), (c("lround"), "W", true), (c("__powidf2"), "T", true), (if apple { c("__exp_finite") } else { "__exp_finite@GLIBC_2.15".to_string() }, "U", true), (if apple { c("sqrt") } else { "sqrt@GLIBC_2.2.5".to_string() }, "U", false), (c("fabsf"), "W", false), (c("copysign"), "T", false), (if apple { c("malloc") } else { "malloc@GLIBC_2.2.5".to_string() }, "U", false), ("_ZN3wai8float_repro3exp17h0123456789abcdefE".to_string(), "T", false), (c("expand_macro"), "T", false), ] { assert_eq!(forbidden(&sym, kind), want, "classifier on {sym:?} ({kind})"); } } #[cfg(unix)] fn main() { link_the_decoder(); check_the_classifier(); let exe = std::env::current_exe().expect("path of this test executable"); let out = std::process::Command::new("nm") .arg("-P") .arg(&exe) .output() .unwrap_or_else(|e| panic!("cannot run `nm` ({e}); it ships with the linker toolchain \ (binutils, or the Xcode command line tools)")); assert!(out.status.success(), "nm -P {} failed: {}", exe.display(), String::from_utf8_lossy(&out.stderr)); let listing = String::from_utf8_lossy(&out.stdout); // `nm -P`: one symbol per line, `name type [value [size]]`. let symbols: Vec<(&str, &str)> = listing .lines() .filter_map(|l| { let mut f = l.split_whitespace(); Some((f.next()?, f.next()?)) }) .collect(); let decoder_linked = symbols.iter().any(|(s, _)| s.contains("13jpegai_decode6decode")); let imports_listed = symbols.iter().any(|(s, t)| *t == "U" && c_name(s) == "malloc"); assert!(decoder_linked, "{}: no `jpegai_decode::decode` symbol among {} listed, so this check cannot say \ what the decoder links (a stripped executable?)", exe.display(), symbols.len()); assert!(imports_listed, "{}: `malloc` is not listed as an import among {} symbols, so the listing does not \ show imports", exe.display(), symbols.len()); let describe = |(s, t): &(&str, &str)| { format!("{s} ({})", if is_import(t) { "imported" } else { "defined here" }) }; let found: Vec = symbols.iter().filter(|(s, t)| forbidden(s, t)).map(describe).collect(); assert!(found.is_empty(), "the JPEG AI exact decoder's executable holds C math-library functions: {}. Something \ the decode reaches calls the platform math library; use wai::float_repro or basic \ operations instead", found.join(", ")); let allowed: Vec = symbols .iter() .filter(|(s, t)| is_math_function(c_name(s)) && !forbidden(s, t)) .map(describe) .collect(); let allowed = if allowed.is_empty() { String::new() } else { format!(" (allowed, exactly specified: {})", allowed.join(", ")) }; eprintln!("jpegai_decode_symbols: {} symbols in {}; the decoder is linked; no C \ math-library function the test forbids among them{allowed}", symbols.len(), exe.display()); } #[cfg(not(unix))] fn main() { link_the_decoder(); check_the_classifier(); eprintln!("jpegai_decode_symbols: SKIPPED, not a pass: this check reads the executable's \ symbols with `nm`, which it runs on unix targets only"); }