合并主分支
Project CI / Repository checks (pull_request) Failing after 11s
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / Frontend tests (pull_request) Successful in 3m1s
Project CI / Native shell tests (pull_request) Successful in 13m12s

同步主分支资源卡依赖关系与类型分类预览等最新变更
保留并补齐画布快速编辑前后端正向白名单
拒绝图片编辑接口处理单个图标、角色动作、音频、视频及未知类型
修正快速编辑决策记录与现行编辑器文档
补充后端现役类型和未知类型表驱动测试
This commit is contained in:
2026-08-05 20:14:45 +08:00
76 changed files with 9334 additions and 1261 deletions
@@ -2709,28 +2709,43 @@ fn align_editor_image_edit_dimension(value: u32) -> u32 {
value.saturating_add(15) / 16 * 16
}
fn ensure_editor_image_edit_asset_kind_allowed(asset_kind: Option<&str>) -> Result<(), AppError> {
let Some(asset_kind) = asset_kind.map(str::trim).filter(|value| !value.is_empty()) else {
return Ok(());
};
if asset_kind != "icon" {
fn ensure_editor_image_edit_source_kind_allowed(
asset_kind: Option<&str>,
media_type: Option<&str>,
) -> Result<(), AppError> {
let asset_kind = asset_kind.map(str::trim).filter(|value| !value.is_empty());
let media_type = media_type.map(str::trim).filter(|value| !value.is_empty());
let asset_kind_allowed = matches!(
asset_kind,
None | Some(
"spec"
| "character"
| "icon-spritesheet"
| "icon-spec"
| "publication-material"
| "ui-design"
)
);
let media_type_allowed = matches!(media_type, None | Some("image"));
if asset_kind_allowed && media_type_allowed {
return Ok(());
}
Err(
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "editor-image-edit",
"message": "单个图标不支持快速编辑",
"message": "当前素材类型不支持图片快速编辑",
"assetKind": asset_kind,
"mediaType": media_type,
})),
)
}
fn resolve_editor_image_edit_target_layer_asset_kind<'a>(
fn resolve_editor_image_edit_target_layer_source_kind<'a>(
layers: &'a Value,
resources: &'a [EditorProjectResourcePayload],
target_layer_id: &str,
) -> Result<Option<&'a str>, AppError> {
) -> Result<(Option<&'a str>, Option<&'a str>), AppError> {
let target_layer = layers
.as_array()
.and_then(|items| {
@@ -2745,9 +2760,22 @@ fn resolve_editor_image_edit_target_layer_asset_kind<'a>(
"targetLayerId": target_layer_id,
}))
})?;
let media_type = match target_layer.get("mediaType") {
Some(Value::String(value)) if !value.trim().is_empty() => Some(value.trim()),
Some(Value::Null) | None => None,
Some(_) => {
return Err(
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "editor-image-edit",
"message": "快速编辑目标图层的媒体类型无效",
"targetLayerId": target_layer_id,
})),
);
}
};
match target_layer.get("assetKindOverride") {
Some(Value::String(value)) if !value.trim().is_empty() => {
return Ok(Some(value.trim()));
return Ok((Some(value.trim()), media_type));
}
Some(Value::Null) | None => {}
Some(_) => {
@@ -2761,12 +2789,13 @@ fn resolve_editor_image_edit_target_layer_asset_kind<'a>(
}
}
let resource_id = target_layer.get("resourceId").and_then(Value::as_str);
Ok(resource_id.and_then(|resource_id| {
let asset_kind = resource_id.and_then(|resource_id| {
resources
.iter()
.find(|resource| resource.resource_id == resource_id)
.and_then(|resource| resource.asset_kind.as_deref())
}))
});
Ok((asset_kind, media_type))
}
async fn ensure_editor_image_edit_source_allowed(
@@ -2780,7 +2809,7 @@ async fn ensure_editor_image_edit_source_allowed(
.map(str::trim)
.filter(|value| !value.is_empty())
else {
return ensure_editor_image_edit_asset_kind_allowed(payload.asset_kind.as_deref());
return ensure_editor_image_edit_source_kind_allowed(payload.asset_kind.as_deref(), None);
};
let Some(target_layer_id) = payload
.target_layer_id
@@ -2788,7 +2817,7 @@ async fn ensure_editor_image_edit_source_allowed(
.map(str::trim)
.filter(|value| !value.is_empty())
else {
return ensure_editor_image_edit_asset_kind_allowed(payload.asset_kind.as_deref());
return ensure_editor_image_edit_source_kind_allowed(payload.asset_kind.as_deref(), None);
};
let project = state
.spacetime_client()
@@ -2799,13 +2828,15 @@ async fn ensure_editor_image_edit_source_allowed(
.await
.map_err(map_editor_project_error)?;
let project = editor_project_payload_from_record(project);
let asset_kind = resolve_editor_image_edit_target_layer_asset_kind(
let (asset_kind, media_type) = resolve_editor_image_edit_target_layer_source_kind(
&project.layers,
&project.resources,
target_layer_id,
)?
.or(payload.asset_kind.as_deref());
ensure_editor_image_edit_asset_kind_allowed(asset_kind)
)?;
ensure_editor_image_edit_source_kind_allowed(
asset_kind.or(payload.asset_kind.as_deref()),
media_type,
)
}
fn encode_editor_image_png(
@@ -13422,35 +13453,54 @@ mod tests {
}
#[test]
fn editor_image_edit_rejects_individual_icons_but_allows_spritesheets_and_specs() {
let error = ensure_editor_image_edit_asset_kind_allowed(Some("icon"))
fn editor_image_edit_source_kind_whitelist_covers_current_and_unknown_types() {
let cases = [
(None, None, true),
(None, Some("image"), true),
(Some("spec"), Some("image"), true),
(Some("character"), Some("image"), true),
(Some("icon-spritesheet"), Some("image"), true),
(Some("icon-spec"), Some("image"), true),
(Some("publication-material"), Some("image"), true),
(Some("ui-design"), Some("image"), true),
(Some("icon"), Some("image"), false),
(Some("character-animation"), Some("image-sequence"), false),
(Some("sound-effect"), Some("audio"), false),
(Some("background-music"), Some("audio"), false),
(Some("video"), Some("video"), false),
(Some("future-asset-kind"), Some("image"), false),
(None, Some("future-media-type"), false),
];
for (asset_kind, media_type, expected_allowed) in cases {
let result = ensure_editor_image_edit_source_kind_allowed(asset_kind, media_type);
assert_eq!(
result.is_ok(),
expected_allowed,
"unexpected whitelist result for assetKind={asset_kind:?}, mediaType={media_type:?}",
);
}
let error = ensure_editor_image_edit_source_kind_allowed(Some("icon"), Some("image"))
.expect_err("individual icon assets should not support quick edit");
assert_eq!(error.status_code(), StatusCode::BAD_REQUEST);
assert_eq!(
error.details().and_then(|details| details.get("message")),
Some(&json!("单个图标不支持快速编辑")),
Some(&json!("当前素材类型不支持图片快速编辑")),
);
assert_eq!(
error.details().and_then(|details| details.get("assetKind")),
Some(&json!("icon")),
);
assert!(ensure_editor_image_edit_asset_kind_allowed(Some("icon-spritesheet")).is_ok());
assert!(ensure_editor_image_edit_asset_kind_allowed(Some("icon-spec")).is_ok());
assert!(ensure_editor_image_edit_asset_kind_allowed(Some("image")).is_ok());
assert!(ensure_editor_image_edit_asset_kind_allowed(None).is_ok());
let layers = json!([
{
"layerId": "layer-icon",
"resourceId": "resource-default-spec",
"assetKind": "character",
"assetKindOverride": "icon"
"assetKindOverride": "icon",
"mediaType": "image"
},
{
"layerId": "layer-icon-spec",
"resourceId": "resource-icon-spec",
"assetKind": "icon"
"assetKind": "icon",
"mediaType": "image"
}
]);
let resources = vec![
@@ -13458,19 +13508,23 @@ mod tests {
editor_project_resource_for_canvas_test("resource-icon-spec", "icon-spec", 512, 512),
];
let icon_kind =
resolve_editor_image_edit_target_layer_asset_kind(&layers, &resources, "layer-icon")
resolve_editor_image_edit_target_layer_source_kind(&layers, &resources, "layer-icon")
.expect("icon layer should exist");
assert!(ensure_editor_image_edit_asset_kind_allowed(icon_kind).is_err());
let icon_spec_kind = resolve_editor_image_edit_target_layer_asset_kind(
assert_eq!(icon_kind, (Some("icon"), Some("image")));
assert!(ensure_editor_image_edit_source_kind_allowed(icon_kind.0, icon_kind.1).is_err());
let icon_spec_kind = resolve_editor_image_edit_target_layer_source_kind(
&layers,
&resources,
"layer-icon-spec",
)
.expect("icon spec layer should exist");
assert_eq!(icon_spec_kind, Some("icon-spec"));
assert!(ensure_editor_image_edit_asset_kind_allowed(icon_spec_kind).is_ok());
assert_eq!(icon_spec_kind, (Some("icon-spec"), Some("image")));
assert!(
resolve_editor_image_edit_target_layer_asset_kind(&layers, &[], "missing").is_err()
ensure_editor_image_edit_source_kind_allowed(icon_spec_kind.0, icon_spec_kind.1)
.is_ok()
);
assert!(
resolve_editor_image_edit_target_layer_source_kind(&layers, &[], "missing").is_err()
);
let invalid_override = json!([{
"layerId": "layer-invalid",
@@ -13478,7 +13532,7 @@ mod tests {
"assetKindOverride": ""
}]);
assert!(
resolve_editor_image_edit_target_layer_asset_kind(
resolve_editor_image_edit_target_layer_source_kind(
&invalid_override,
&resources,
"layer-invalid",