收口双端分享目标有效性校验

移动壳 share.setTarget 只缓存可生成分享内容的目标

桌面壳 share.setTarget 写缓存前校验目标有效性

双端配置检查反查分享目标有效性边界
This commit is contained in:
2026-06-20 04:25:31 +08:00
parent 038b5d995d
commit ed05b410c9
5 changed files with 67 additions and 1 deletions
@@ -1550,6 +1550,8 @@ for (const expectedShareSnippet of [
'DesktopSharePayload::Invalid',
'"share target is invalid"',
'set_desktop_host_bridge_share_target',
'share_text_from_value(target)',
'share_target_payload_must_be_valid_before_cache',
'open_desktop_host_bridge_share',
'write_desktop_clipboard_text(app, &share_text)',
'"copied_to_clipboard"',
@@ -144,6 +144,23 @@ pub(crate) fn set_desktop_host_bridge_share_target(
let Some(target) = target else {
return failed(request.id.clone(), "invalid_request", "target is required");
};
match share_text_from_value(target) {
DesktopSharePayload::Valid(_) => {}
DesktopSharePayload::Invalid => {
return failed(
request.id.clone(),
"invalid_request",
"share target is invalid",
)
}
DesktopSharePayload::Empty => {
return failed(
request.id.clone(),
"invalid_request",
"share target is required",
)
}
}
let share_state = app.state::<DesktopShareState>();
let response = match share_state.target.lock() {
@@ -305,6 +322,28 @@ mod tests {
);
}
#[test]
fn share_target_payload_must_be_valid_before_cache() {
assert!(matches!(
share_text_from_value(&json!({})),
DesktopSharePayload::Empty
));
assert!(matches!(
share_text_from_value(&json!({
"title": "危险作品",
"url": "https://example.com/works/detail?work=PZ-1"
})),
DesktopSharePayload::Invalid
));
assert!(matches!(
share_text_from_value(&json!({
"title": "测试作品",
"work": "PZ-1"
})),
DesktopSharePayload::Valid(_)
));
}
#[test]
fn share_text_requires_payload_or_stored_target() {
let state = DesktopShareState::default();
@@ -1319,6 +1319,8 @@ for (const snippet of [
'setMobileHostBridgeShareTarget',
'request: HostBridgeRequest',
"throw invalidRequest('target is required')",
'const normalizedTarget = normalizeHostBridgeShareOpenPayload(target);',
"'share target is invalid'",
'ok(request, true)',
'resetMobileHostBridgeShareTargetForTest',
]) {
@@ -922,7 +922,7 @@ describe('handleMobileHostBridgeMessage', () => {
});
});
test('share.setTarget 拒绝缺少目标且不清空已有目标', async () => {
test('share.setTarget 拒绝缺少或无效目标且不清空已有目标', async () => {
expectOk(
await send(
request('share.setTarget', {
@@ -941,6 +941,21 @@ describe('handleMobileHostBridgeMessage', () => {
expect(expectFailed(invalid).error.message).toBe('target is required');
const empty = await send(request('share.setTarget', { target: {} }));
expect(expectFailed(empty).error.message).toBe('share target is required');
const unsafe = await send(
request('share.setTarget', {
target: {
title: '危险作品',
url: 'https://example.com/works/detail?work=PZ-1',
},
}),
);
expect(expectFailed(unsafe).error.message).toBe('share target is invalid');
expectOk(await send(request('share.open')));
expect(Share.share).toHaveBeenCalledWith({
title: '暖灯猫街',
@@ -17,6 +17,14 @@ export function setMobileHostBridgeShareTarget(request: HostBridgeRequest) {
if (target === undefined) {
throw invalidRequest('target is required');
}
const normalizedTarget = normalizeHostBridgeShareOpenPayload(target);
if (normalizedTarget.status !== 'valid') {
throw invalidRequest(
normalizedTarget.status === 'invalid'
? 'share target is invalid'
: 'share target is required',
);
}
currentShareTarget = target;
return ok(request, true);