diff --git a/server-rs/crates/platform-tripo/src/common/types.rs b/server-rs/crates/platform-tripo/src/common/types.rs index 20836123c..0600e6d36 100644 --- a/server-rs/crates/platform-tripo/src/common/types.rs +++ b/server-rs/crates/platform-tripo/src/common/types.rs @@ -16,6 +16,10 @@ use crate::{ /// 从远端地址推导出的扩展名上限;超出即按不可信处理,退回默认扩展名。 const MAX_ARTIFACT_FILE_EXTENSION_LEN: usize = 10; +/// 文件名主干上限。主干同样来自远端(`TripoArtifactBytes::filename` 是公开 API), +/// 不截断就可能拼出超过文件系统名字上限的一长串。 +const MAX_ARTIFACT_FILE_STEM_LEN: usize = 128; + #[derive(Clone, Debug, Eq, PartialEq)] pub struct TripoTaskHandle { pub task_id: String, @@ -297,7 +301,7 @@ fn fallback_artifact_extension(content_type: Option<&str>) -> &'static str { } /// 文件名主干同样来自不可信输入:路径分隔符与 `..` 会让拼接结果逃出目标目录, -/// 这里统一中性化成安全字符。 +/// 这里统一中性化成安全字符,并截到文件系统友好的一段长度。 fn sanitize_artifact_file_stem(name: &str) -> String { let sanitized: String = name .chars() @@ -308,6 +312,7 @@ fn sanitize_artifact_file_stem(name: &str) -> String { '_' } }) + .take(MAX_ARTIFACT_FILE_STEM_LEN) .collect(); if sanitized.trim_matches('_').is_empty() { "artifact".to_string() @@ -315,3 +320,32 @@ fn sanitize_artifact_file_stem(name: &str) -> String { sanitized } } + +#[cfg(test)] +mod tests { + use super::*; + + fn artifact_url(path: &str) -> TripoUrl { + TripoUrl::parse(&format!("https://cdn.example.com{path}")).expect("夹具地址必须合法") + } + + #[test] + fn path_like_and_oversized_stems_are_neutralized_and_capped() { + let name = format!("{}../../etc/passwd", "a".repeat(400)); + let file_name = + artifact_filename(&name, Some("model/gltf-binary"), &artifact_url("/a.glb")); + + let stem = file_name.strip_suffix(".glb").expect("扩展名应取自 URL"); + assert_eq!(stem.chars().count(), MAX_ARTIFACT_FILE_STEM_LEN); + assert!(!stem.contains('/'), "路径分隔符必须被中性化:{stem}"); + assert!(!stem.contains('.'), "`..` 必须被中性化:{stem}"); + } + + #[test] + fn blank_stem_falls_back_to_a_fixed_name() { + assert_eq!( + artifact_filename("..", Some("model/gltf-binary"), &artifact_url("/a.glb")), + "artifact.glb" + ); + } +}