修复手动去背景输入校验
增加手动去背景源对象的 OSS 大小和类型预检。 使用 Range 文件头和图片魔数校验,避免非法对象进入 BgFilter retry。 补充内容类型、魔数匹配和调用顺序测试。
This commit is contained in:
@@ -2987,6 +2987,7 @@ pub(crate) async fn remove_editor_image_background_for_owner(
|
||||
payload.source_image_src.as_str(),
|
||||
)
|
||||
.await?;
|
||||
validate_editor_background_removal_source(state, source_object_key.as_str()).await?;
|
||||
let removed =
|
||||
request_editor_background_removal_image_with_retry(state, source_object_key.as_str())
|
||||
.await?;
|
||||
@@ -3083,6 +3084,121 @@ struct EditorBackgroundRemovalImage {
|
||||
height: u32,
|
||||
}
|
||||
|
||||
async fn validate_editor_background_removal_source(
|
||||
state: &AppState,
|
||||
source_object_key: &str,
|
||||
) -> Result<(), AppError> {
|
||||
let oss_client = state.oss_client().ok_or_else(|| {
|
||||
AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_details(json!({
|
||||
"provider": "aliyun-oss",
|
||||
"reason": "OSS 未完成环境变量配置",
|
||||
}))
|
||||
})?;
|
||||
let http_client = reqwest::Client::new();
|
||||
let head = oss_client
|
||||
.head_object(
|
||||
&http_client,
|
||||
OssHeadObjectRequest {
|
||||
object_key: source_object_key.to_string(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|error| map_oss_error(error, "aliyun-oss"))?;
|
||||
if head.content_length > EDITOR_REFERENCE_IMAGE_MAX_SIZE_BYTES {
|
||||
return Err(editor_reference_image_too_large());
|
||||
}
|
||||
|
||||
let content_type =
|
||||
normalize_editor_background_removal_source_mime_type(head.content_type.as_deref())
|
||||
.ok_or_else(|| {
|
||||
editor_background_removal_source_unsupported(
|
||||
"图片参考图的 Content-Type 不是支持的图片类型。",
|
||||
)
|
||||
})?;
|
||||
let signed_url = sign_editor_private_object_read_url(
|
||||
state,
|
||||
source_object_key,
|
||||
EDITOR_MATTING_SOURCE_URL_EXPIRE_SECONDS,
|
||||
)?;
|
||||
let probe_end = EDITOR_BACKGROUND_REMOVAL_SOURCE_PROBE_BYTES.saturating_sub(1);
|
||||
let response = http_client
|
||||
.get(signed_url)
|
||||
.header(reqwest::header::RANGE, format!("bytes=0-{probe_end}"))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "aliyun-oss",
|
||||
"message": format!("读取图片参考图文件头失败:{error}"),
|
||||
}))
|
||||
})?;
|
||||
if response.status() != StatusCode::PARTIAL_CONTENT {
|
||||
return Err(
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "aliyun-oss",
|
||||
"message": "OSS 未按 Range 返回图片文件头。",
|
||||
"status": response.status().as_u16(),
|
||||
})),
|
||||
);
|
||||
}
|
||||
if response
|
||||
.content_length()
|
||||
.is_none_or(|size| size > EDITOR_BACKGROUND_REMOVAL_SOURCE_PROBE_BYTES)
|
||||
{
|
||||
return Err(
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "aliyun-oss",
|
||||
"message": "OSS 返回的图片文件头大小异常。",
|
||||
})),
|
||||
);
|
||||
}
|
||||
let probe = response.bytes().await.map_err(|error| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "aliyun-oss",
|
||||
"message": format!("读取图片参考图文件头失败:{error}"),
|
||||
}))
|
||||
})?;
|
||||
validate_editor_background_removal_source_content(content_type, probe.as_ref())
|
||||
}
|
||||
|
||||
fn validate_editor_background_removal_source_content(
|
||||
content_type: &str,
|
||||
bytes: &[u8],
|
||||
) -> Result<(), AppError> {
|
||||
let detected_type = infer_editor_reference_image_mime_type(bytes).ok_or_else(|| {
|
||||
editor_background_removal_source_unsupported("图片参考图文件魔数不是支持的图片类型。")
|
||||
})?;
|
||||
if detected_type != content_type {
|
||||
return Err(editor_background_removal_source_unsupported(
|
||||
"图片参考图的 Content-Type 与文件魔数不一致。",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn normalize_editor_background_removal_source_mime_type(
|
||||
content_type: Option<&str>,
|
||||
) -> Option<&'static str> {
|
||||
let mime_type = content_type?.split(';').next()?.trim();
|
||||
if mime_type.eq_ignore_ascii_case("image/png") {
|
||||
Some("image/png")
|
||||
} else if mime_type.eq_ignore_ascii_case("image/jpeg") {
|
||||
Some("image/jpeg")
|
||||
} else if mime_type.eq_ignore_ascii_case("image/webp") {
|
||||
Some("image/webp")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn editor_background_removal_source_unsupported(message: &str) -> AppError {
|
||||
AppError::from_status(StatusCode::UNSUPPORTED_MEDIA_TYPE).with_details(json!({
|
||||
"provider": "editor-reference-image",
|
||||
"field": "sourceImageSrc",
|
||||
"message": message,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn request_editor_background_removal_image_with_retry(
|
||||
state: &AppState,
|
||||
source_object_key: &str,
|
||||
@@ -7220,6 +7336,7 @@ async fn persist_editor_provider_source_resource(
|
||||
|
||||
const EDITOR_REFERENCE_IMAGE_READ_EXPIRE_SECONDS: u64 = 300;
|
||||
const EDITOR_REFERENCE_IMAGE_MAX_SIZE_BYTES: u64 = 32 * 1024 * 1024;
|
||||
const EDITOR_BACKGROUND_REMOVAL_SOURCE_PROBE_BYTES: u64 = 16;
|
||||
const EDITOR_BACKGROUND_REMOVAL_MAX_RESPONSE_BYTES: usize = 32 * 1024 * 1024;
|
||||
const EDITOR_BACKGROUND_REMOVAL_MAX_IMAGE_DIMENSION: u32 = 8192;
|
||||
|
||||
@@ -9902,6 +10019,49 @@ mod tests {
|
||||
assert_eq!(infer_editor_reference_image_mime_type(b"not image"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_background_removal_source_requires_supported_content_type_and_matching_magic() {
|
||||
assert_eq!(
|
||||
normalize_editor_background_removal_source_mime_type(Some("IMAGE/PNG; charset=utf-8")),
|
||||
Some("image/png")
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_editor_background_removal_source_mime_type(Some("image/gif")),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_editor_background_removal_source_mime_type(Some("application/pdf")),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_editor_background_removal_source_mime_type(None),
|
||||
None
|
||||
);
|
||||
|
||||
assert!(
|
||||
validate_editor_background_removal_source_content(
|
||||
"image/png",
|
||||
b"\x89PNG\r\n\x1a\nrest"
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
assert_eq!(
|
||||
validate_editor_background_removal_source_content(
|
||||
"image/png",
|
||||
&[0xff, 0xd8, 0xff, 0xe0]
|
||||
)
|
||||
.expect_err("mismatched image content should be rejected")
|
||||
.status_code(),
|
||||
StatusCode::UNSUPPORTED_MEDIA_TYPE
|
||||
);
|
||||
assert_eq!(
|
||||
validate_editor_background_removal_source_content("image/png", b"not image")
|
||||
.expect_err("non-image content should be rejected")
|
||||
.status_code(),
|
||||
StatusCode::UNSUPPORTED_MEDIA_TYPE
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_character_postprocess_fallback_response_keeps_source_and_warning() {
|
||||
let response = EditorImageGenerationResponse {
|
||||
@@ -10610,6 +10770,7 @@ mod tests {
|
||||
&[
|
||||
"caller.report_processing_phase(state).await?",
|
||||
"resolve_editor_reference_object_key_for_owner",
|
||||
"validate_editor_background_removal_source",
|
||||
"request_editor_background_removal_image_with_retry",
|
||||
],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user