diff --git a/server-rs/crates/platform-tripo/src/common/types.rs b/server-rs/crates/platform-tripo/src/common/types.rs index 455779e29..327ac73a7 100644 --- a/server-rs/crates/platform-tripo/src/common/types.rs +++ b/server-rs/crates/platform-tripo/src/common/types.rs @@ -197,7 +197,42 @@ impl TripoDownloadedArtifact { && extension.len() <= MAX_ARTIFACT_FILE_EXTENSION_LEN && extension.bytes().all(|byte| byte.is_ascii_alphanumeric()) }) - .unwrap_or("glb"); - format!("{name}.{extension}") + .unwrap_or_else(|| fallback_artifact_extension(self.content_type.as_deref())); + format!("{}.{extension}", sanitize_artifact_file_stem(name)) + } +} + +/// URL 路径给不出可用扩展名时按 content type 兜底:同一条下载路径也服务预览图, +/// 一律写 `.glb` 会把 PNG / JPEG 产物写成模型文件名。 +fn fallback_artifact_extension(content_type: Option<&str>) -> &'static str { + let content_type = content_type.unwrap_or_default(); + if content_type.starts_with("image/png") { + "png" + } else if content_type.starts_with("image/jpeg") { + "jpg" + } else if content_type.starts_with("image/webp") { + "webp" + } else { + "glb" + } +} + +/// 文件名主干同样来自不可信输入:路径分隔符与 `..` 会让拼接结果逃出目标目录, +/// 这里统一中性化成安全字符。 +fn sanitize_artifact_file_stem(name: &str) -> String { + let sanitized: String = name + .chars() + .map(|ch| { + if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_') { + ch + } else { + '_' + } + }) + .collect(); + if sanitized.trim_matches('_').is_empty() { + "artifact".to_string() + } else { + sanitized } }