@@ -22,8 +22,11 @@ const OCR_MAX_WIDTH: u32 = 960;
2222const MIN_SCORE : f32 = 0.5 ;
2323/// 识别预览条数上限(前端即时反馈)。
2424const PREVIEW_MAX : usize = 3 ;
25- /// 单图 base64 解码后字节上限(16MB——解码前拒绝,省 CPU)。
26- const MAX_PHOTO_BYTES : usize = 16 * 1024 * 1024 ;
25+ /// base64 串长度上限(16MB 字节 → base64 膨胀 4/3——解码前拒绝,省 CPU)。
26+ const MAX_PHOTO_B64_LEN : usize = 16 * 1024 * 1024 * 4 / 3 + 4 ;
27+ /// 解码后像素数上限(40MP——压缩炸弹防御:小体积 PNG 可解出超大位图,
28+ /// to_rgb8 前拒绝,避免数百 MB 内存分配)。
29+ const MAX_PIXELS : u64 = 40_000_000 ;
2730
2831/// 截图保存结果(前端反馈契约;camelCase 与前端 PhotoResult 对齐)。
2932#[ derive( Debug , Clone , PartialEq , Serialize ) ]
@@ -75,10 +78,10 @@ pub fn save_photo_capture(
7578 if b. score < MIN_SCORE || b. text . trim ( ) . is_empty ( ) {
7679 continue ;
7780 }
78- count += 1 ;
7981 if preview. len ( ) < PREVIEW_MAX {
8082 preview. push ( b. text . clone ( ) ) ;
8183 }
84+ // 审查修复:成功落库才计数(与 import_frame 链路口径一致——失败不虚高)
8285 if let Err ( e) = db. add_ocr_block ( & NewSessionOcrBlock {
8386 session_id,
8487 timestamp_ms,
@@ -95,28 +98,37 @@ pub fn save_photo_capture(
9598 screen_id : None ,
9699 } ) {
97100 eprintln ! ( "[Photo] OCR 块落库失败: {}" , e) ;
101+ } else {
102+ count += 1 ;
98103 }
99104 }
100105 Ok ( PhotoCaptureResult { duplicated : false , block_count : count, preview, image_ref : rel } )
101106}
102107
103108/// base64 解码 + 解码器验证(白名单由 image 解码器把关——capture_fragment 同模式)。
104109fn decode_image ( image_b64 : & str ) -> Result < image:: RgbImage , String > {
105- let bytes = base64:: engine:: general_purpose:: STANDARD
106- . decode ( image_b64. trim ( ) )
107- . map_err ( |e| format ! ( "图片 base64 解码失败: {}" , e) ) ?;
108- if bytes. is_empty ( ) {
110+ let trimmed = image_b64. trim ( ) ;
111+ if trimmed. is_empty ( ) {
109112 return Err ( "图片数据为空" . to_string ( ) ) ;
110113 }
111- if bytes. len ( ) > MAX_PHOTO_BYTES {
112- return Err ( format ! ( "图片超限(>{}MB)" , MAX_PHOTO_BYTES / 1024 / 1024 ) ) ;
114+ // 审查修复:解码前按 base64 长度预估拒绝(16MB 字节上限——省解码 CPU)
115+ if trimmed. len ( ) > MAX_PHOTO_B64_LEN {
116+ return Err ( format ! ( "图片超限(>{}MB)" , MAX_PHOTO_B64_LEN / 4 * 3 / 1024 / 1024 ) ) ;
113117 }
118+ let bytes = base64:: engine:: general_purpose:: STANDARD
119+ . decode ( trimmed)
120+ . map_err ( |e| format ! ( "图片 base64 解码失败: {}" , e) ) ?;
114121 let img = image:: load_from_memory ( & bytes)
115122 . map_err ( |e| format ! ( "图片解码失败(仅支持 PNG/JPEG 等常见格式): {}" , e) ) ?;
116123 let ( w, h) = img. dimensions ( ) ;
117124 if w == 0 || h == 0 {
118125 return Err ( "图片尺寸非法" . to_string ( ) ) ;
119126 }
127+ // 审查修复:像素数上限(压缩炸弹防御——小体积 PNG 解出超大位图时,
128+ // to_rgb8 的 w*h*3 分配可达数百 MB,解码后立即拒绝)
129+ if ( w as u64 ) * ( h as u64 ) > MAX_PIXELS {
130+ return Err ( format ! ( "图片像素超限(>{}MP)" , MAX_PIXELS / 1_000_000 ) ) ;
131+ }
120132 Ok ( img. to_rgb8 ( ) )
121133}
122134
0 commit comments