收紧导入结果消费边界

将文本、文档、图片和音频导入结果归一化提升到共享 HostBridge 契约
让 H5 facade 消费宿主导入结果前复核文件名、MIME、字节数和 base64 载荷
移除 H5 facade 本地导入 MIME Set 并用总门禁反查共享 normalizer
增加导入结果边界测试和超限结果回退覆盖
同步宿主壳协议文档和共享决策记录
This commit is contained in:
2026-06-19 16:34:35 +08:00
parent a68377596a
commit d7af659aa5
7 changed files with 388 additions and 153 deletions
@@ -2052,6 +2052,39 @@ describe('hostBridge', () => {
await expect(importHostDocumentFile()).resolves.toBe(false);
});
test('原生 App 宿主导入结果超出共享边界时回退为 false', async () => {
const invoke = vi.fn(
async (_command: string, args?: Record<string, unknown>) => {
const request = (args as { request: { id: string } }).request;
return {
bridge: 'GenarrativeHostBridge',
version: 1,
id: request.id,
ok: true,
result: {
action: 'selected',
fileName: 'too-large.webm',
base64Data: 'YXVkaW8=',
mimeType: 'audio/webm',
bytes: 20 * 1024 * 1024 + 1,
},
};
},
);
window.history.replaceState(
null,
'',
nativeAppPath(['file.importAudio']),
);
window.__TAURI__ = {
core: {
invoke: asTauriInvoke(invoke),
},
};
await expect(importHostAudioFile()).resolves.toBe(false);
});
test('原生 App 宿主取消导入图片时回退为 false', async () => {
const invoke = vi.fn(
async (_command: string, args?: Record<string, unknown>) => {
+15 -144
View File
@@ -48,6 +48,10 @@ import {
normalizeHostBridgeExportTextPayload,
normalizeHostBridgeExternalUrlPayload,
normalizeHostBridgeHapticsImpactStyle,
normalizeHostBridgeImportAudioResult,
normalizeHostBridgeImportDocumentResult,
normalizeHostBridgeImportImageResult,
normalizeHostBridgeImportTextResult,
normalizeHostBridgeLifecycleState,
normalizeHostBridgeLocalNotification,
normalizeHostBridgeQrCodeValue,
@@ -907,64 +911,19 @@ export async function exportHostAudioFile(
}
}
const HOST_BRIDGE_IMAGE_MIME_TYPE_SET = new Set(HOST_BRIDGE_IMAGE_MIME_TYPES);
function normalizeHostImageImportResult(
payload: FileImportImageResult | null | undefined,
): FileImportImageResult | false {
if (
!payload ||
typeof payload !== 'object' ||
payload.action !== 'selected' &&
payload.action !== 'dropped' &&
payload.action !== 'captured' ||
!HOST_BRIDGE_IMAGE_MIME_TYPE_SET.has(payload.mimeType) ||
typeof payload.fileName !== 'string' ||
!payload.fileName.trim() ||
typeof payload.base64Data !== 'string' ||
!payload.base64Data.trim() ||
typeof payload.bytes !== 'number' ||
!Number.isInteger(payload.bytes) ||
payload.bytes <= 0
) {
return false;
}
const position =
payload.position &&
typeof payload.position.x === 'number' &&
Number.isFinite(payload.position.x) &&
typeof payload.position.y === 'number' &&
Number.isFinite(payload.position.y)
? {
x: payload.position.x,
y: payload.position.y,
}
: undefined;
return {
action: payload.action,
fileName: payload.fileName.trim(),
base64Data: payload.base64Data,
mimeType: payload.mimeType,
bytes: payload.bytes,
...(position ? { position } : {}),
};
}
export async function importHostImageFile() {
if (!canUseNativeHostCapability('file.importImage')) {
return false;
}
try {
return normalizeHostImageImportResult(
return normalizeHostBridgeImportImageResult(
await requestNativeAppHostBridge<FileImportImageResult>(
'file.importImage',
undefined,
{ timeoutMs: HOST_BRIDGE_USER_INTERACTION_TIMEOUT_MS },
),
);
) ?? false;
} catch (error) {
if (isUnsupportedHostBridgeError(error)) {
return false;
@@ -979,13 +938,13 @@ export async function captureHostImageFile() {
}
try {
return normalizeHostImageImportResult(
return normalizeHostBridgeImportImageResult(
await requestNativeAppHostBridge<FileImportImageResult>(
'file.captureImage',
undefined,
{ timeoutMs: HOST_BRIDGE_USER_INTERACTION_TIMEOUT_MS },
),
);
) ?? false;
} catch (error) {
if (isUnsupportedHostBridgeError(error)) {
return false;
@@ -1018,49 +977,19 @@ export async function scanHostQrCode() {
}
}
const HOST_BRIDGE_TEXT_MIME_TYPE_SET = new Set(HOST_BRIDGE_TEXT_MIME_TYPES);
const HOST_BRIDGE_DOCUMENT_MIME_TYPE_SET = new Set(HOST_BRIDGE_DOCUMENT_MIME_TYPES);
function normalizeHostTextImportResult(
payload: FileImportTextResult | null | undefined,
): FileImportTextResult | false {
if (
!payload ||
typeof payload !== 'object' ||
payload.action !== 'selected' ||
!HOST_BRIDGE_TEXT_MIME_TYPE_SET.has(payload.mimeType) ||
typeof payload.fileName !== 'string' ||
!payload.fileName.trim() ||
typeof payload.content !== 'string' ||
typeof payload.bytes !== 'number' ||
!Number.isInteger(payload.bytes) ||
payload.bytes <= 0
) {
return false;
}
return {
action: 'selected',
fileName: payload.fileName.trim(),
content: payload.content,
mimeType: payload.mimeType,
bytes: payload.bytes,
};
}
export async function importHostTextFile() {
if (!canUseNativeHostCapability('file.importText')) {
return false;
}
try {
return normalizeHostTextImportResult(
return normalizeHostBridgeImportTextResult(
await requestNativeAppHostBridge<FileImportTextResult>(
'file.importText',
undefined,
{ timeoutMs: HOST_BRIDGE_USER_INTERACTION_TIMEOUT_MS },
),
);
) ?? false;
} catch (error) {
if (isUnsupportedHostBridgeError(error)) {
return false;
@@ -1069,47 +998,19 @@ export async function importHostTextFile() {
}
}
function normalizeHostDocumentImportResult(
payload: FileImportDocumentResult | null | undefined,
): FileImportDocumentResult | false {
if (
!payload ||
typeof payload !== 'object' ||
payload.action !== 'selected' ||
!HOST_BRIDGE_DOCUMENT_MIME_TYPE_SET.has(payload.mimeType) ||
typeof payload.fileName !== 'string' ||
!payload.fileName.trim() ||
typeof payload.base64Data !== 'string' ||
!payload.base64Data.trim() ||
typeof payload.bytes !== 'number' ||
!Number.isInteger(payload.bytes) ||
payload.bytes <= 0
) {
return false;
}
return {
action: 'selected',
fileName: payload.fileName.trim(),
base64Data: payload.base64Data,
mimeType: payload.mimeType,
bytes: payload.bytes,
};
}
export async function importHostDocumentFile() {
if (!canUseNativeHostCapability('file.importDocument')) {
return false;
}
try {
return normalizeHostDocumentImportResult(
return normalizeHostBridgeImportDocumentResult(
await requestNativeAppHostBridge<FileImportDocumentResult>(
'file.importDocument',
undefined,
{ timeoutMs: HOST_BRIDGE_USER_INTERACTION_TIMEOUT_MS },
),
);
) ?? false;
} catch (error) {
if (isUnsupportedHostBridgeError(error)) {
return false;
@@ -1118,49 +1019,19 @@ export async function importHostDocumentFile() {
}
}
const HOST_BRIDGE_AUDIO_MIME_TYPE_SET = new Set(HOST_BRIDGE_AUDIO_MIME_TYPES);
function normalizeHostAudioImportResult(
payload: FileImportAudioResult | null | undefined,
): FileImportAudioResult | false {
if (
!payload ||
typeof payload !== 'object' ||
payload.action !== 'selected' ||
!HOST_BRIDGE_AUDIO_MIME_TYPE_SET.has(payload.mimeType) ||
typeof payload.fileName !== 'string' ||
!payload.fileName.trim() ||
typeof payload.base64Data !== 'string' ||
!payload.base64Data.trim() ||
typeof payload.bytes !== 'number' ||
!Number.isInteger(payload.bytes) ||
payload.bytes <= 0
) {
return false;
}
return {
action: 'selected',
fileName: payload.fileName.trim(),
base64Data: payload.base64Data,
mimeType: payload.mimeType,
bytes: payload.bytes,
};
}
export async function importHostAudioFile() {
if (!canUseNativeHostCapability('file.importAudio')) {
return false;
}
try {
return normalizeHostAudioImportResult(
return normalizeHostBridgeImportAudioResult(
await requestNativeAppHostBridge<FileImportAudioResult>(
'file.importAudio',
undefined,
{ timeoutMs: HOST_BRIDGE_USER_INTERACTION_TIMEOUT_MS },
),
);
) ?? false;
} catch (error) {
if (isUnsupportedHostBridgeError(error)) {
return false;
@@ -1365,7 +1236,7 @@ export function subscribeHostImageDrop(
return subscribeNativeAppHostBridgeEvent<FileImportImageResult>(
'file.imageDropped',
(payload) => {
const normalizedPayload = normalizeHostImageImportResult(payload);
const normalizedPayload = normalizeHostBridgeImportImageResult(payload);
if (normalizedPayload) {
listener(normalizedPayload);
}