From bab265e38a665f4e7375af2030a1550008494de4 Mon Sep 17 00:00:00 2001 From: simpleqt <89645338+simpleqt@users.noreply.github.com> Date: Sun, 20 Sep 2026 20:47:00 +0800 Subject: [PATCH 1/2] fix(image): accept case/space variants of the data URL base64 marker in the size precheck MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit estimated_decoded_len detected the base64 marker with an exact ends_with(";base64") compare, but DataUrl::process accepts the marker ASCII-case-insensitively and with optional spaces after the semicolon. For 'data:image/webp;BASE64,...' or '...; base64,...' the estimator therefore took the percent-encoding branch and returned the raw base64 length (about 4/3 of the decoded size), falsely rejecting every image whose decoded size sits in the top quarter of the allowed range — with the default 32 MiB limit, a valid ~25.5 MB image reported ImageTooLarge { size: 34_000_000 }. The identical body with a lowercase, space-free marker was accepted. The marker check now trims and compares ASCII-case-insensitively, matching the parser both ways use. --- deepseek-recipe-image/src/resolver.rs | 36 ++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/deepseek-recipe-image/src/resolver.rs b/deepseek-recipe-image/src/resolver.rs index d92aa92..1f0833e 100644 --- a/deepseek-recipe-image/src/resolver.rs +++ b/deepseek-recipe-image/src/resolver.rs @@ -228,16 +228,50 @@ fn estimated_decoded_len(data_url: &str) -> usize { let Some((header, body)) = data_url.split_once(',') else { return 0; }; - if header.ends_with(";base64") { + if is_base64_header(header) { body.len() / 4 * 3 } else { body.len().saturating_sub(2 * body.matches('%').count()) } } +/// Whether the data URL header declares a base64 body. +/// +/// `DataUrl::process` accepts the marker ASCII-case-insensitively with +/// optional surrounding spaces (`;BASE64`, `; base64`); an exact +/// `ends_with(";base64")` compare took the percent-encoding branch for those +/// and estimated the raw base64 length (about 4/3 of the decoded size), +/// falsely rejecting in-limit images near the top of the allowed range. +fn is_base64_header(header: &str) -> bool { + header + .rsplit_once(';') + .is_some_and(|(_, param)| param.trim().eq_ignore_ascii_case("base64")) +} + #[cfg(test)] mod tests { use std::sync::Arc; + + #[test] + fn base64_marker_detection_matches_the_parser() { + // 与 data-url crate 的解析一致:大小写不敏感、允许分号后空格。 + assert!(is_base64_header("data:image/webp;base64")); + assert!(is_base64_header("data:image/webp;BASE64")); + assert!(is_base64_header("data:image/webp; base64")); + assert!(!is_base64_header("data:image/webp")); + assert!(!is_base64_header("data:image/webp;charset=utf8")); + } + + #[test] + fn estimated_decoded_len_uses_base64_math_for_case_or_space_variants() { + // 34_000_000 个 base64 字符 ≈ 25.5MB 解码后大小,应按 base64 公式估算 + // 而不是把 base64 长度当作解码长度。 + let body = "A".repeat(34_000_000); + for header in ["data:image/webp;base64", "data:image/webp;BASE64", "data:image/webp; base64"] { + let data_url = format!("{header},{body}"); + assert_eq!(estimated_decoded_len(&data_url), 34_000_000 / 4 * 3); + } + } use std::sync::atomic::{AtomicUsize, Ordering}; use deepseek_recipe_core::multimodal::{ImageDetail, ImageInfo}; From 0032df61cbea4231c170af0880e705141693de1a Mon Sep 17 00:00:00 2001 From: simpleqt <89645338+simpleqt@users.noreply.github.com> Date: Sun, 20 Sep 2026 20:47:17 +0800 Subject: [PATCH 2/2] style: rustfmt --- deepseek-recipe-image/src/resolver.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/deepseek-recipe-image/src/resolver.rs b/deepseek-recipe-image/src/resolver.rs index 1f0833e..f166475 100644 --- a/deepseek-recipe-image/src/resolver.rs +++ b/deepseek-recipe-image/src/resolver.rs @@ -267,7 +267,11 @@ mod tests { // 34_000_000 个 base64 字符 ≈ 25.5MB 解码后大小,应按 base64 公式估算 // 而不是把 base64 长度当作解码长度。 let body = "A".repeat(34_000_000); - for header in ["data:image/webp;base64", "data:image/webp;BASE64", "data:image/webp; base64"] { + for header in [ + "data:image/webp;base64", + "data:image/webp;BASE64", + "data:image/webp; base64", + ] { let data_url = format!("{header},{body}"); assert_eq!(estimated_decoded_len(&data_url), 34_000_000 / 4 * 3); }