diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index d580360d3..a4accea7d 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -2104,6 +2104,20 @@ if ( ) { throw new Error('mobile shell image export must reject empty and oversized bytes'); } +const exportTextFileBody = extractFunctionBody(filesSource, 'exportTextFile'); +const exportTextMimeValidationIndex = exportTextFileBody.indexOf( + 'HOST_BRIDGE_TEXT_MIME_TYPE_SET.has(mimeType as HostBridgeTextMimeType)', +); +const exportTextSharingAvailabilityIndex = exportTextFileBody.indexOf( + 'await assertMobileFileSharingAvailable()', +); +if ( + exportTextMimeValidationIndex < 0 || + exportTextSharingAvailabilityIndex < 0 || + exportTextMimeValidationIndex > exportTextSharingAvailabilityIndex +) { + throw new Error('mobile shell text export must validate MIME before native sharing checks'); +} for (const [wrapperName, fileCall] of [ ['exportMobileHostBridgeTextFile', 'ok(request, await exportTextFile(request.payload))'], @@ -2126,6 +2140,7 @@ for (const snippet of [ 'exportTextFile({', 'exportImageFile({', 'rejects every export before cache writes when system sharing is unavailable', + 'rejects invalid text export MIME before touching native sharing', 'const exportCases = [', 'expect(writtenFiles).toHaveLength(0)', 'importTextFile()', diff --git a/apps/mobile-shell/src/host-bridge/files.test.ts b/apps/mobile-shell/src/host-bridge/files.test.ts index 1d6c8deda..a202f40db 100644 --- a/apps/mobile-shell/src/host-bridge/files.test.ts +++ b/apps/mobile-shell/src/host-bridge/files.test.ts @@ -185,6 +185,23 @@ describe('mobile HostBridge file actions', () => { expect(shareAsyncMock).not.toHaveBeenCalled(); }); + test('rejects invalid text export MIME before touching native sharing', async () => { + await expect( + exportTextFile({ + content: '泥巴AI', + fileName: '创作记录', + mimeType: 'application/octet-stream', + }), + ).rejects.toMatchObject({ + code: 'invalid_request', + message: 'mimeType must be an allowed text type', + }); + + expect(shareAvailableMock).not.toHaveBeenCalled(); + expect(writtenFiles).toHaveLength(0); + expect(shareAsyncMock).not.toHaveBeenCalled(); + }); + test('maps native sharing availability failures to stable export errors', async () => { shareAvailableMock.mockRejectedValueOnce(new Error('expo sharing crashed')); diff --git a/apps/mobile-shell/src/host-bridge/files.ts b/apps/mobile-shell/src/host-bridge/files.ts index c0c5fd51f..c05252064 100644 --- a/apps/mobile-shell/src/host-bridge/files.ts +++ b/apps/mobile-shell/src/host-bridge/files.ts @@ -109,8 +109,6 @@ export async function exportTextFile( throw invalidRequest('content exceeds file export size limit'); } - await assertMobileFileSharingAvailable(); - const fileName = normalizeHostBridgeExportFileName(exportPayload?.fileName); const rawMimeType = exportPayload?.mimeType; const mimeType = @@ -120,6 +118,9 @@ export async function exportTextFile( if (!HOST_BRIDGE_TEXT_MIME_TYPE_SET.has(mimeType as HostBridgeTextMimeType)) { throw invalidRequest('mimeType must be an allowed text type'); } + + await assertMobileFileSharingAvailable(); + try { const file = new File(Paths.cache, fileName); file.write(content);