Recently, I ran Claude Code against image-rs to look for potential issues.
I previously did something similar for lofty, and a surprisingly large portion of the findings turned out to be legitimate, ranging from incorrect comments to actual logic bugs.
Looking through the image-rs results with a non-expert eye, my rough impression is that around half of the findings are probably nitpicks or false positives, while the other half may point to real problems and likely deserve manual human review.
Example of findings
C2. AVIF SMPTE 240M kr/kb coefficients are swapped
- File:
src/codecs/avif/yuv.rs:226-229
- Confirmed in code.
YuvStandardMatrix::Smpte240 => YuvBias {
kr: 0.087f32,
kb: 0.212f32,
},
SMPTE 240M defines Y = 0.212·R + 0.701·G + 0.087·B. The literal for kr (red coefficient) is 0.087, which is actually the blue coefficient. As a result, any AVIF tagged with MatrixCoefficients::ST170M or ST240M (mapped to this matrix in decoder.rs:321-326) decodes with red and blue swapped in the inverse YUV→RGB transform — i.e. a heavy magenta/teal tint.
L2. AVIF YCgCo macro doc says "420" for 422 codec
- File:
src/codecs/avif/ycgco.rs:324-331
define_ycgco_half_chroma!(
ycgco422_to_rgba8, yuv422_to_rgbx_invoker,
u8, RGBA_CN, 8,
"Converts YCgCo 420 8-bit planar format to Rgba 8-bit"
);
Function and invoker are 4:2:2 but doc says 4:2:0.
M15. Format-detection hooks accept empty signatures (matches every input)
- File:
src/hooks.rs:152-162 and :165-183
pub fn register_format_detection_hook(mut extension: OsString, signature: &'static [u8],
mask: Option<&'static [u8]>) {
extension.make_ascii_lowercase();
GUESS_FORMAT_HOOKS.write().unwrap().push((signature, mask.unwrap_or(&[]), extension));
}
// matcher (:177-180):
if mask.is_empty() {
if start.starts_with(signature) { return Some(extension.clone()); }
}
An empty signature makes start.starts_with(&[]) always true, so the hook claims every stream. Hooks run before built-ins, so this also lets a library shadow PNG / JPEG / etc. by prefix. Combined with M16 (MAGIC_BYTES for PNM are only 2 bytes), the hook system is exploitable for "format hijacking" by any crate that registers first.
Fix: reject empty signatures (return bool from the registration function).
M9. LimitError::Display for Unsupported prints nothing and contains a typo
- File:
src/error.rs:522-533
LimitErrorKind::Unsupported { .. } => {
write!(fmt, "The following strict limits are specified but not supported by the opertation: ")?;
Ok(())
}
The colon is left dangling, the user never sees what the unsupported limits are, and "opertation" is misspelled. Two-for-one cosmetic + diagnostic bug.
Fix: include the actual limits/supported fields in the output; fix the typo.
L30. gaussian_blur_dyn_image repeats ~14 lines per variant (10×)
- File:
src/imageops/sample.rs:1285-1426
Each DynamicImage variant repeats vec![0u8; img.subpixels().len()] and an .unwrap(). ~140 lines that could be ~30 with a macro.
Full report - findings.md
Recently, I ran Claude Code against
image-rsto look for potential issues.I previously did something similar for
lofty, and a surprisingly large portion of the findings turned out to be legitimate, ranging from incorrect comments to actual logic bugs.Looking through the
image-rsresults with a non-expert eye, my rough impression is that around half of the findings are probably nitpicks or false positives, while the other half may point to real problems and likely deserve manual human review.Example of findings
C2. AVIF SMPTE 240M
kr/kbcoefficients are swappedsrc/codecs/avif/yuv.rs:226-229SMPTE 240M defines
Y = 0.212·R + 0.701·G + 0.087·B. The literal forkr(red coefficient) is0.087, which is actually the blue coefficient. As a result, any AVIF tagged withMatrixCoefficients::ST170MorST240M(mapped to this matrix indecoder.rs:321-326) decodes with red and blue swapped in the inverse YUV→RGB transform — i.e. a heavy magenta/teal tint.L2. AVIF YCgCo macro doc says "420" for 422 codec
src/codecs/avif/ycgco.rs:324-331Function and invoker are 4:2:2 but doc says 4:2:0.
M15. Format-detection hooks accept empty signatures (matches every input)
src/hooks.rs:152-162and:165-183An empty
signaturemakesstart.starts_with(&[])alwaystrue, so the hook claims every stream. Hooks run before built-ins, so this also lets a library shadow PNG / JPEG / etc. by prefix. Combined with M16 (MAGIC_BYTESfor PNM are only 2 bytes), the hook system is exploitable for "format hijacking" by any crate that registers first.Fix: reject empty signatures (return
boolfrom the registration function).M9.
LimitError::DisplayforUnsupportedprints nothing and contains a typosrc/error.rs:522-533The colon is left dangling, the user never sees what the unsupported limits are, and "opertation" is misspelled. Two-for-one cosmetic + diagnostic bug.
Fix: include the actual
limits/supportedfields in the output; fix the typo.L30.
gaussian_blur_dyn_imagerepeats ~14 lines per variant (10×)src/imageops/sample.rs:1285-1426Each
DynamicImagevariant repeatsvec![0u8; img.subpixels().len()]and an.unwrap(). ~140 lines that could be ~30 with a macro.Full report - findings.md