收紧媒体导出载荷边界
将图片和音频导出载荷预校验提升到共享 HostBridge 契约 让 H5 facade 按共享媒体导出边界归一化文件名、MIME 和 base64 载荷 保留 Expo 与 Tauri 壳对真实字节和 MIME 的二次校验 增加媒体导出载荷测试和三端壳总门禁反查 同步宿主壳协议文档和共享决策记录
This commit is contained in:
@@ -37,7 +37,9 @@ import {
|
||||
normalizeHostBridgeClipboardText,
|
||||
normalizeHostBridgeColorScheme,
|
||||
normalizeHostBridgeConnectionType,
|
||||
normalizeHostBridgeExportAudioPayload,
|
||||
normalizeHostBridgeExportFileName,
|
||||
normalizeHostBridgeExportImagePayload,
|
||||
normalizeHostBridgeExternalUrl,
|
||||
normalizeHostBridgeExternalUrlPayload,
|
||||
normalizeHostBridgeHapticsImpactStyle,
|
||||
@@ -349,6 +351,45 @@ describe('HostBridge shared contract helpers', () => {
|
||||
expect(HOST_BRIDGE_IMPORT_DOCUMENT_MAX_BYTES).toBe(5 * 1024 * 1024);
|
||||
});
|
||||
|
||||
test('归一化宿主图片和音频导出载荷', () => {
|
||||
expect(
|
||||
normalizeHostBridgeExportImagePayload({
|
||||
fileName: ' ../分享:卡?.png ',
|
||||
base64Data: ' c2hhcmU= ',
|
||||
mimeType: 'image/png',
|
||||
}),
|
||||
).toEqual({
|
||||
fileName: '分享-卡-.png',
|
||||
base64Data: 'c2hhcmU=',
|
||||
mimeType: 'image/png',
|
||||
});
|
||||
expect(
|
||||
normalizeHostBridgeExportAudioPayload({
|
||||
fileName: '敲击音效.wav',
|
||||
base64Data: ' YXVkaW8= ',
|
||||
mimeType: 'audio/wav',
|
||||
}),
|
||||
).toEqual({
|
||||
fileName: '敲击音效.wav',
|
||||
base64Data: 'YXVkaW8=',
|
||||
mimeType: 'audio/wav',
|
||||
});
|
||||
expect(
|
||||
normalizeHostBridgeExportImagePayload({
|
||||
fileName: 'bad.gif',
|
||||
base64Data: 'c2hhcmU=',
|
||||
mimeType: 'image/gif',
|
||||
}),
|
||||
).toBeNull();
|
||||
expect(
|
||||
normalizeHostBridgeExportAudioPayload({
|
||||
fileName: 'empty.wav',
|
||||
base64Data: ' ',
|
||||
mimeType: 'audio/wav',
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
test('归一化宿主本地通知内容', () => {
|
||||
expect(
|
||||
normalizeHostBridgeLocalNotification({
|
||||
|
||||
@@ -573,6 +573,10 @@ export const HOST_BRIDGE_IMAGE_MIME_TYPES = [
|
||||
'image/webp',
|
||||
] as const satisfies readonly HostBridgeImageMimeType[];
|
||||
|
||||
const HOST_BRIDGE_IMAGE_MIME_TYPE_SET = new Set<HostBridgeImageMimeType>(
|
||||
HOST_BRIDGE_IMAGE_MIME_TYPES,
|
||||
);
|
||||
|
||||
export type FileImportImageResult = {
|
||||
action: 'selected' | 'dropped' | 'captured';
|
||||
fileName: string;
|
||||
@@ -625,6 +629,10 @@ export const HOST_BRIDGE_AUDIO_MIME_TYPES = [
|
||||
'audio/webm',
|
||||
] as const satisfies readonly HostBridgeAudioMimeType[];
|
||||
|
||||
const HOST_BRIDGE_AUDIO_MIME_TYPE_SET = new Set<HostBridgeAudioMimeType>(
|
||||
HOST_BRIDGE_AUDIO_MIME_TYPES,
|
||||
);
|
||||
|
||||
export type FileImportAudioResult = {
|
||||
action: 'selected';
|
||||
fileName: string;
|
||||
@@ -653,6 +661,80 @@ export const HOST_BRIDGE_IMPORT_IMAGE_MAX_BYTES = 10 * 1024 * 1024;
|
||||
export const HOST_BRIDGE_EXPORT_AUDIO_MAX_BYTES = 20 * 1024 * 1024;
|
||||
export const HOST_BRIDGE_IMPORT_AUDIO_MAX_BYTES = 20 * 1024 * 1024;
|
||||
|
||||
function normalizeHostBridgeBase64Data(rawData: unknown) {
|
||||
if (typeof rawData !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const base64Data = rawData.trim();
|
||||
if (!base64Data || hasHostBridgeControlCharacter(base64Data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return base64Data;
|
||||
}
|
||||
|
||||
function estimateHostBridgeBase64Bytes(base64Data: string) {
|
||||
const normalizedLength = base64Data.replace(/\s+/g, '').length;
|
||||
return Math.ceil((normalizedLength * 3) / 4);
|
||||
}
|
||||
|
||||
export function normalizeHostBridgeExportImagePayload(
|
||||
payload: unknown,
|
||||
): FileExportImagePayload | null {
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const candidate = payload as Partial<FileExportImagePayload>;
|
||||
const mimeType = candidate.mimeType as HostBridgeImageMimeType;
|
||||
if (!HOST_BRIDGE_IMAGE_MIME_TYPE_SET.has(mimeType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const base64Data = normalizeHostBridgeBase64Data(candidate.base64Data);
|
||||
if (
|
||||
!base64Data ||
|
||||
estimateHostBridgeBase64Bytes(base64Data) > HOST_BRIDGE_EXPORT_IMAGE_MAX_BYTES
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
fileName: normalizeHostBridgeExportFileName(candidate.fileName),
|
||||
base64Data,
|
||||
mimeType,
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeHostBridgeExportAudioPayload(
|
||||
payload: unknown,
|
||||
): FileExportAudioPayload | null {
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const candidate = payload as Partial<FileExportAudioPayload>;
|
||||
const mimeType = candidate.mimeType as HostBridgeAudioMimeType;
|
||||
if (!HOST_BRIDGE_AUDIO_MIME_TYPE_SET.has(mimeType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const base64Data = normalizeHostBridgeBase64Data(candidate.base64Data);
|
||||
if (
|
||||
!base64Data ||
|
||||
estimateHostBridgeBase64Bytes(base64Data) > HOST_BRIDGE_EXPORT_AUDIO_MAX_BYTES
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
fileName: normalizeHostBridgeExportFileName(candidate.fileName),
|
||||
base64Data,
|
||||
mimeType,
|
||||
};
|
||||
}
|
||||
|
||||
export const HOST_BRIDGE_HAPTICS_IMPACT_STYLES = [
|
||||
'light',
|
||||
'medium',
|
||||
|
||||
Reference in New Issue
Block a user