From 75ec3361dc6b84aa7f4bcfcda55ab94f16885976 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Fri, 18 Sep 2026 14:12:47 +0800 Subject: [PATCH] =?UTF-8?q?=E9=99=90=E5=88=B6=E9=99=84=E4=BB=B6=E5=85=83?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E6=A0=BC=E5=BC=8F=E4=B8=8E=E9=95=BF=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 校验 inline 附件名称和媒体类型的控制字符、格式与长度。 共享附件清洗上限常量并补充异常元数据拒绝测试。 --- .../src/agent/direct_codex_attachments.rs | 6 +- .../direct_codex_user_item/validation.rs | 67 ++++++++++++++++++- .../src/agent/direct_codex_user_item/wire.rs | 9 ++- 3 files changed, 73 insertions(+), 9 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_attachments.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_attachments.rs index 8cf49e017..2f93f775f 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_attachments.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_attachments.rs @@ -2,9 +2,9 @@ //! 有项目路径或导入状态时输出路径映射;否则保持首页元数据文案。不灌正文。 pub(crate) const MAX_DIRECT_CODEX_ATTACHMENTS: usize = 8; -const MAX_DIRECT_CODEX_ATTACHMENT_NAME_CHARS: usize = 160; -const MAX_DIRECT_CODEX_ATTACHMENT_MEDIA_TYPE_CHARS: usize = 96; -const MAX_DIRECT_CODEX_ATTACHMENT_LOCAL_PATH_CHARS: usize = 512; +pub(crate) const MAX_DIRECT_CODEX_ATTACHMENT_NAME_CHARS: usize = 160; +pub(crate) const MAX_DIRECT_CODEX_ATTACHMENT_MEDIA_TYPE_CHARS: usize = 96; +pub(crate) const MAX_DIRECT_CODEX_ATTACHMENT_LOCAL_PATH_CHARS: usize = 512; const HOME_ATTACHMENT_HEADER: &str = "[首页附件说明:当前尚未打开项目,以下仅为附件元数据,附件内容尚不可读取]"; diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/validation.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/validation.rs index f0ff6e7be..acbbdb4f0 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/validation.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/validation.rs @@ -4,7 +4,8 @@ use super::model::{ }; use crate::agent::{ read_manifest_for_project, sanitize_attachment_local_path, GameCreationAppManifest, - MAX_DIRECT_CODEX_ATTACHMENTS, + MAX_DIRECT_CODEX_ATTACHMENTS, MAX_DIRECT_CODEX_ATTACHMENT_MEDIA_TYPE_CHARS, + MAX_DIRECT_CODEX_ATTACHMENT_NAME_CHARS, }; use std::path::Path; @@ -50,6 +51,22 @@ pub(crate) fn validate_direct_codex_user_item( if reference.name.trim().is_empty() { return Err("附件缺少文件名".to_string()); } + let name = reference.name.trim(); + if name.chars().count() > MAX_DIRECT_CODEX_ATTACHMENT_NAME_CHARS + || name.chars().any(char::is_control) + { + return Err("附件文件名无效或过长".to_string()); + } + let media_type = reference.media_type.trim(); + if media_type.is_empty() + || media_type.chars().count() > MAX_DIRECT_CODEX_ATTACHMENT_MEDIA_TYPE_CHARS + || media_type.chars().any(|character| { + !(character.is_ascii_alphanumeric() + || matches!(character, '/' | '+' | '-' | '.' | '_')) + }) + { + return Err("附件媒体类型无效或过长".to_string()); + } let status = reference.status.trim(); if status == "imported" && reference.local_path.trim().is_empty() { return Err("已导入附件缺少项目路径".to_string()); @@ -214,4 +231,52 @@ mod tests { .expect_err("imported attachment without a project path must fail"); assert!(error.contains("缺少项目路径"), "{error}"); } + + #[test] + fn attachment_name_and_media_type_are_bounded_and_well_formed() { + let root = tempfile::tempdir().expect("temp project"); + crate::init_local_game_project_at(root.path(), "validation-test", "校验测试") + .expect("init project"); + let long_name = "a".repeat(crate::agent::MAX_DIRECT_CODEX_ATTACHMENT_NAME_CHARS + 1); + let cases = [ + ( + json!({ + "name": "bad\nname.txt", + "mediaType": "text/plain" + }), + "文件名", + ), + ( + json!({ + "name": "ok.txt", + "mediaType": "text/plain\nsecret" + }), + "媒体类型", + ), + ( + json!({ + "name": long_name, + "mediaType": "text/plain" + }), + "文件名", + ), + ]; + for (metadata, expected) in cases { + let mut value = metadata; + value["type"] = json!("agc_attachment_reference"); + value["size"] = json!(1); + value["localPath"] = json!(""); + value["status"] = json!("failed"); + let item = serde_json::from_value(json!({ + "type": "message", + "role": "user", + "content": [value], + "id": "turn-1:user" + })) + .expect("deserialize user item"); + let error = validate_direct_codex_user_item(root.path(), &item) + .expect_err("invalid attachment metadata must fail"); + assert!(error.contains(expected), "{error}"); + } + } } diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/wire.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/wire.rs index ca26742f5..d9b365dd8 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/wire.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/wire.rs @@ -227,8 +227,8 @@ mod tests { "id": "turn-1:user", "content": [{ "type": "agc_attachment_reference", - "name": "C:\\tmp\\notes\nsecret.md", - "mediaType": "text/plain\nBearer secret", + "name": "C:\\tmp\\notes.md", + "mediaType": "text/plain", "size": 4, "localPath": "assets\\.\\notes.txt", "status": "imported" @@ -240,10 +240,9 @@ mod tests { ) .expect("attachment metadata should project"); let text = wire[0]["text"].as_str().expect("wire text"); - assert!(text.contains("名称=notessecret.md"), "{text}"); - assert!(text.contains("类型=application/octet-stream"), "{text}"); + assert!(text.contains("名称=notes.md"), "{text}"); + assert!(text.contains("类型=text/plain"), "{text}"); assert!(text.contains("项目路径=assets/notes.txt"), "{text}"); - assert!(!text.contains("Bearer secret"), "{text}"); } #[test]