移动壳放宽音频选择器候选

移动端音频导入的 DocumentPicker 候选加入 audio/*

补充泛 MIME 仍按扩展名和 bytes 校验的移动壳测试

移动壳配置门禁锁定音频选择器类型和 bytes 校验

同步宿主壳方案文档和共享决策日志
This commit is contained in:
2026-06-18 23:12:28 +08:00
parent 6c17b4917f
commit dbcfa51191
5 changed files with 64 additions and 8 deletions
@@ -67,6 +67,9 @@ const WAV_BASE64 = Buffer.from('RIFF\x04\x00\x00\x00WAVE', 'binary').toString(
const MP3_BASE64 = Buffer.from('ID3\x04\x00\x00\x00\x00\x00\x10', 'binary').toString(
'base64',
);
const MP4_AUDIO_BASE64 = encodeBytes([
0, 0, 0, 0x18, 0x66, 0x74, 0x79, 0x70, 0x4d, 0x34, 0x41, 0x20,
]);
const WEBM_BASE64 = encodeBytes([0x1a, 0x45, 0xdf, 0xa3, 0x01, 0x00]);
vi.mock('expo-clipboard', () => ({
@@ -1370,6 +1373,7 @@ describe('handleMobileHostBridgeMessage', () => {
copyToCacheDirectory: true,
multiple: false,
type: [
'audio/*',
'audio/mpeg',
'audio/mp4',
'audio/wav',
@@ -1379,6 +1383,52 @@ describe('handleMobileHostBridgeMessage', () => {
});
});
test('file.importAudio 允许系统 MIME 缺失时按扩展名和 bytes 校验导入', async () => {
fileBase64Data.set('file:///private/mobile/hit.m4a', MP3_BASE64);
vi.mocked(DocumentPicker.getDocumentAsync).mockResolvedValue({
canceled: false,
assets: [
{
uri: 'file:///private/mobile/hit.m4a',
name: 'hit.m4a',
mimeType: 'application/octet-stream',
size: 10,
lastModified: 1,
},
],
});
const mismatched = await send(request('file.importAudio'));
expect(expectFailed(mismatched).error.message).toBe(
'audio bytes do not match MIME',
);
fileBase64Data.set('file:///private/mobile/hit.m4a', MP4_AUDIO_BASE64);
vi.mocked(DocumentPicker.getDocumentAsync).mockResolvedValue({
canceled: false,
assets: [
{
uri: 'file:///private/mobile/hit.m4a',
name: 'hit.m4a',
mimeType: 'application/octet-stream',
size: 12,
lastModified: 1,
},
],
});
const imported = await send(request('file.importAudio'));
expect(expectOk(imported).result).toEqual({
action: 'selected',
fileName: 'hit.m4a',
base64Data: MP4_AUDIO_BASE64,
mimeType: 'audio/mp4',
bytes: 12,
});
});
test('file.importAudio 取消选择并拒绝非法 MIME 与超限音频', async () => {
vi.mocked(DocumentPicker.getDocumentAsync).mockResolvedValue({
canceled: true,
+9 -7
View File
@@ -45,6 +45,14 @@ const HOST_BRIDGE_AUDIO_MIME_TYPES = new Set<HostBridgeAudioMimeType>([
'audio/ogg',
'audio/webm',
]);
export const MOBILE_AUDIO_DOCUMENT_PICKER_TYPES = [
'audio/*',
'audio/mpeg',
'audio/mp4',
'audio/wav',
'audio/ogg',
'audio/webm',
];
const BASE64_ALPHABET =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
@@ -617,13 +625,7 @@ export async function importAudioFile(): Promise<FileImportAudioResult> {
const result = await DocumentPicker.getDocumentAsync({
copyToCacheDirectory: true,
multiple: false,
type: [
'audio/mpeg',
'audio/mp4',
'audio/wav',
'audio/ogg',
'audio/webm',
],
type: MOBILE_AUDIO_DOCUMENT_PICKER_TYPES,
});
if (result.canceled) {
throw {