收紧导入结果消费边界
将文本、文档、图片和音频导入结果归一化提升到共享 HostBridge 契约 让 H5 facade 消费宿主导入结果前复核文件名、MIME、字节数和 base64 载荷 移除 H5 facade 本地导入 MIME Set 并用总门禁反查共享 normalizer 增加导入结果边界测试和超限结果回退覆盖 同步宿主壳协议文档和共享决策记录
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -44,6 +44,10 @@ import {
|
||||
normalizeHostBridgeExternalUrl,
|
||||
normalizeHostBridgeExternalUrlPayload,
|
||||
normalizeHostBridgeHapticsImpactStyle,
|
||||
normalizeHostBridgeImportAudioResult,
|
||||
normalizeHostBridgeImportDocumentResult,
|
||||
normalizeHostBridgeImportImageResult,
|
||||
normalizeHostBridgeImportTextResult,
|
||||
normalizeHostBridgeLifecycleState,
|
||||
normalizeHostBridgeLocalNotification,
|
||||
normalizeHostBridgeQrCodeValue,
|
||||
@@ -418,6 +422,91 @@ describe('HostBridge shared contract helpers', () => {
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
test('归一化宿主文件导入结果', () => {
|
||||
expect(
|
||||
normalizeHostBridgeImportTextResult({
|
||||
action: 'selected',
|
||||
fileName: ' 剧情.md ',
|
||||
content: '暖灯猫街',
|
||||
mimeType: 'text/markdown',
|
||||
bytes: 12,
|
||||
}),
|
||||
).toEqual({
|
||||
action: 'selected',
|
||||
fileName: '剧情.md',
|
||||
content: '暖灯猫街',
|
||||
mimeType: 'text/markdown',
|
||||
bytes: 12,
|
||||
});
|
||||
expect(
|
||||
normalizeHostBridgeImportDocumentResult({
|
||||
action: 'selected',
|
||||
fileName: ' 世界设定.docx ',
|
||||
base64Data: ' UEsDBGRvY3g= ',
|
||||
mimeType:
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
bytes: 8,
|
||||
}),
|
||||
).toEqual({
|
||||
action: 'selected',
|
||||
fileName: '世界设定.docx',
|
||||
base64Data: 'UEsDBGRvY3g=',
|
||||
mimeType:
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
bytes: 8,
|
||||
});
|
||||
expect(
|
||||
normalizeHostBridgeImportImageResult({
|
||||
action: 'dropped',
|
||||
fileName: ' 参考图.png ',
|
||||
base64Data: ' aW1hZ2U= ',
|
||||
mimeType: 'image/png',
|
||||
bytes: 5,
|
||||
position: { x: 12, y: 24 },
|
||||
}),
|
||||
).toEqual({
|
||||
action: 'dropped',
|
||||
fileName: '参考图.png',
|
||||
base64Data: 'aW1hZ2U=',
|
||||
mimeType: 'image/png',
|
||||
bytes: 5,
|
||||
position: { x: 12, y: 24 },
|
||||
});
|
||||
expect(
|
||||
normalizeHostBridgeImportAudioResult({
|
||||
action: 'selected',
|
||||
fileName: ' 敲击音效.webm ',
|
||||
base64Data: ' YXVkaW8= ',
|
||||
mimeType: 'audio/webm',
|
||||
bytes: 5,
|
||||
}),
|
||||
).toEqual({
|
||||
action: 'selected',
|
||||
fileName: '敲击音效.webm',
|
||||
base64Data: 'YXVkaW8=',
|
||||
mimeType: 'audio/webm',
|
||||
bytes: 5,
|
||||
});
|
||||
expect(
|
||||
normalizeHostBridgeImportTextResult({
|
||||
action: 'selected',
|
||||
fileName: 'bad.bin',
|
||||
content: 'content',
|
||||
mimeType: 'application/octet-stream',
|
||||
bytes: 7,
|
||||
}),
|
||||
).toBeNull();
|
||||
expect(
|
||||
normalizeHostBridgeImportAudioResult({
|
||||
action: 'selected',
|
||||
fileName: 'huge.webm',
|
||||
base64Data: 'YXVkaW8=',
|
||||
mimeType: 'audio/webm',
|
||||
bytes: 20 * 1024 * 1024 + 1,
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
test('归一化宿主本地通知内容', () => {
|
||||
expect(
|
||||
normalizeHostBridgeLocalNotification({
|
||||
|
||||
@@ -708,6 +708,204 @@ export function normalizeHostBridgeExportTextPayload(
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeHostBridgeImportedBytes(rawBytes: unknown, maxBytes: number) {
|
||||
if (
|
||||
typeof rawBytes !== 'number' ||
|
||||
!Number.isInteger(rawBytes) ||
|
||||
rawBytes <= 0 ||
|
||||
rawBytes > maxBytes
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return rawBytes;
|
||||
}
|
||||
|
||||
function normalizeHostBridgeImportFileName(rawFileName: unknown) {
|
||||
if (typeof rawFileName !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fileName = rawFileName.trim();
|
||||
if (!fileName || hasHostBridgeControlCharacter(fileName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return fileName;
|
||||
}
|
||||
|
||||
export function normalizeHostBridgeImportTextResult(
|
||||
payload: unknown,
|
||||
): FileImportTextResult | null {
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const candidate = payload as Partial<FileImportTextResult>;
|
||||
if (candidate.action !== 'selected') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fileName = normalizeHostBridgeImportFileName(candidate.fileName);
|
||||
const bytes = normalizeHostBridgeImportedBytes(
|
||||
candidate.bytes,
|
||||
HOST_BRIDGE_IMPORT_TEXT_MAX_BYTES,
|
||||
);
|
||||
const mimeType = candidate.mimeType as HostBridgeTextMimeType;
|
||||
if (
|
||||
!fileName ||
|
||||
bytes === null ||
|
||||
!HOST_BRIDGE_TEXT_MIME_TYPE_SET.has(mimeType) ||
|
||||
typeof candidate.content !== 'string' ||
|
||||
estimateHostBridgeUtf8Bytes(candidate.content) >
|
||||
HOST_BRIDGE_IMPORT_TEXT_MAX_BYTES
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
action: 'selected',
|
||||
fileName,
|
||||
content: candidate.content,
|
||||
mimeType,
|
||||
bytes,
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeHostBridgeImportDocumentResult(
|
||||
payload: unknown,
|
||||
): FileImportDocumentResult | null {
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const candidate = payload as Partial<FileImportDocumentResult>;
|
||||
if (candidate.action !== 'selected') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fileName = normalizeHostBridgeImportFileName(candidate.fileName);
|
||||
const bytes = normalizeHostBridgeImportedBytes(
|
||||
candidate.bytes,
|
||||
HOST_BRIDGE_IMPORT_DOCUMENT_MAX_BYTES,
|
||||
);
|
||||
const mimeType = candidate.mimeType as HostBridgeDocumentMimeType;
|
||||
const base64Data = normalizeHostBridgeBase64Data(candidate.base64Data);
|
||||
if (
|
||||
!fileName ||
|
||||
bytes === null ||
|
||||
!HOST_BRIDGE_DOCUMENT_MIME_TYPES.includes(mimeType) ||
|
||||
!base64Data ||
|
||||
estimateHostBridgeBase64Bytes(base64Data) >
|
||||
HOST_BRIDGE_IMPORT_DOCUMENT_MAX_BYTES
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
action: 'selected',
|
||||
fileName,
|
||||
base64Data,
|
||||
mimeType,
|
||||
bytes,
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeHostBridgeImportImageResult(
|
||||
payload: unknown,
|
||||
): FileImportImageResult | null {
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const candidate = payload as Partial<FileImportImageResult>;
|
||||
if (
|
||||
candidate.action !== 'selected' &&
|
||||
candidate.action !== 'dropped' &&
|
||||
candidate.action !== 'captured'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fileName = normalizeHostBridgeImportFileName(candidate.fileName);
|
||||
const bytes = normalizeHostBridgeImportedBytes(
|
||||
candidate.bytes,
|
||||
HOST_BRIDGE_IMPORT_IMAGE_MAX_BYTES,
|
||||
);
|
||||
const mimeType = candidate.mimeType as HostBridgeImageMimeType;
|
||||
const base64Data = normalizeHostBridgeBase64Data(candidate.base64Data);
|
||||
if (
|
||||
!fileName ||
|
||||
bytes === null ||
|
||||
!HOST_BRIDGE_IMAGE_MIME_TYPE_SET.has(mimeType) ||
|
||||
!base64Data ||
|
||||
estimateHostBridgeBase64Bytes(base64Data) >
|
||||
HOST_BRIDGE_IMPORT_IMAGE_MAX_BYTES
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const position =
|
||||
candidate.position &&
|
||||
typeof candidate.position.x === 'number' &&
|
||||
Number.isFinite(candidate.position.x) &&
|
||||
typeof candidate.position.y === 'number' &&
|
||||
Number.isFinite(candidate.position.y)
|
||||
? {
|
||||
x: candidate.position.x,
|
||||
y: candidate.position.y,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
action: candidate.action,
|
||||
fileName,
|
||||
base64Data,
|
||||
mimeType,
|
||||
bytes,
|
||||
...(position ? { position } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeHostBridgeImportAudioResult(
|
||||
payload: unknown,
|
||||
): FileImportAudioResult | null {
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const candidate = payload as Partial<FileImportAudioResult>;
|
||||
if (candidate.action !== 'selected') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fileName = normalizeHostBridgeImportFileName(candidate.fileName);
|
||||
const bytes = normalizeHostBridgeImportedBytes(
|
||||
candidate.bytes,
|
||||
HOST_BRIDGE_IMPORT_AUDIO_MAX_BYTES,
|
||||
);
|
||||
const mimeType = candidate.mimeType as HostBridgeAudioMimeType;
|
||||
const base64Data = normalizeHostBridgeBase64Data(candidate.base64Data);
|
||||
if (
|
||||
!fileName ||
|
||||
bytes === null ||
|
||||
!HOST_BRIDGE_AUDIO_MIME_TYPE_SET.has(mimeType) ||
|
||||
!base64Data ||
|
||||
estimateHostBridgeBase64Bytes(base64Data) >
|
||||
HOST_BRIDGE_IMPORT_AUDIO_MAX_BYTES
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
action: 'selected',
|
||||
fileName,
|
||||
base64Data,
|
||||
mimeType,
|
||||
bytes,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeHostBridgeBase64Data(rawData: unknown) {
|
||||
if (typeof rawData !== 'string') {
|
||||
return null;
|
||||
|
||||
@@ -972,10 +972,6 @@ function assertH5HostBridgePayloadBoundaries() {
|
||||
);
|
||||
|
||||
for (const sharedBoundary of [
|
||||
'HOST_BRIDGE_TEXT_MIME_TYPES',
|
||||
'HOST_BRIDGE_DOCUMENT_MIME_TYPES',
|
||||
'HOST_BRIDGE_IMAGE_MIME_TYPES',
|
||||
'HOST_BRIDGE_AUDIO_MIME_TYPES',
|
||||
'HOST_BRIDGE_RUNTIME_REFRESH_TIMEOUT_MS',
|
||||
'HOST_BRIDGE_USER_INTERACTION_TIMEOUT_MS',
|
||||
'HOST_BRIDGE_SCANNER_TIMEOUT_MS',
|
||||
@@ -1132,6 +1128,54 @@ function assertH5HostBridgePayloadBoundaries() {
|
||||
'H5 HostBridge facade must normalize app.setTitle payloads with the shared title boundary',
|
||||
);
|
||||
}
|
||||
for (const importBoundary of [
|
||||
{
|
||||
functionName: 'importHostTextFile',
|
||||
normalizer: 'normalizeHostBridgeImportTextResult',
|
||||
},
|
||||
{
|
||||
functionName: 'importHostDocumentFile',
|
||||
normalizer: 'normalizeHostBridgeImportDocumentResult',
|
||||
},
|
||||
{
|
||||
functionName: 'importHostImageFile',
|
||||
normalizer: 'normalizeHostBridgeImportImageResult',
|
||||
},
|
||||
{
|
||||
functionName: 'captureHostImageFile',
|
||||
normalizer: 'normalizeHostBridgeImportImageResult',
|
||||
},
|
||||
{
|
||||
functionName: 'importHostAudioFile',
|
||||
normalizer: 'normalizeHostBridgeImportAudioResult',
|
||||
},
|
||||
{
|
||||
functionName: 'subscribeHostImageDrop',
|
||||
normalizer: 'normalizeHostBridgeImportImageResult',
|
||||
},
|
||||
]) {
|
||||
const functionSource = extractFunctionSource(
|
||||
h5HostBridgeSource,
|
||||
importBoundary.functionName,
|
||||
);
|
||||
if (!functionSource.includes(`${importBoundary.normalizer}(`)) {
|
||||
throw new Error(
|
||||
`${importBoundary.functionName} must normalize imported file results with shared ${importBoundary.normalizer}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const localMimeSet of [
|
||||
'HOST_BRIDGE_TEXT_MIME_TYPE_SET',
|
||||
'HOST_BRIDGE_DOCUMENT_MIME_TYPE_SET',
|
||||
'HOST_BRIDGE_IMAGE_MIME_TYPE_SET',
|
||||
'HOST_BRIDGE_AUDIO_MIME_TYPE_SET',
|
||||
]) {
|
||||
if (h5HostBridgeSource.includes(localMimeSet)) {
|
||||
throw new Error(
|
||||
`H5 HostBridge facade must use shared import normalizers instead of local ${localMimeSet}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertH5NativeAppTransportTimeoutBoundaries() {
|
||||
|
||||
@@ -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>) => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user