收紧原生壳载荷边界门禁

导出 HostBridge 文件和通知载荷边界常量
校验移动壳文件大小和 MIME 清单与共享契约一致
校验桌面壳载荷限制和 MIME 清单与共享契约一致
更新原生壳方案和共享决策记录
This commit is contained in:
2026-06-19 00:26:18 +08:00
parent 1f5c752302
commit 759b2cb388
5 changed files with 369 additions and 9 deletions
+202 -5
View File
@@ -645,7 +645,7 @@ function extractStringArrayExport(source, exportName, seen = new Set()) {
}
const match = source.match(
new RegExp(`export const ${exportName}[^=]*= \\[([\\s\\S]*?)\\](?: as const)?;`),
new RegExp(`export const ${exportName}[^=]*= \\[([\\s\\S]*?)\\][^;]*;`),
);
if (!match) {
throw new Error(`unable to read ${exportName}`);
@@ -690,14 +690,14 @@ function extractRustStringConst(source, constName) {
function extractRustNumberConst(source, constName) {
const match = source.match(
new RegExp(
`(?:pub\\(crate\\)\\s+)?const ${constName}\\s*:\\s*(?:u8|u16|u32|usize|i64|i32)\\s*=\\s*(\\d+);`,
`(?:pub\\(crate\\)\\s+)?const ${constName}\\s*:\\s*(?:u8|u16|u32|u64|usize|i64|i32)\\s*=\\s*([^;]+);`,
),
);
if (!match) {
throw new Error(`unable to read Rust const ${constName}`);
}
return Number(match[1]);
return evaluateNumberExpression(match[1]);
}
function extractTsStringConst(source, constName) {
@@ -713,13 +713,54 @@ function extractTsStringConst(source, constName) {
function extractTsNumberConst(source, constName) {
const match = source.match(
new RegExp(`export const ${constName}\\s*=\\s*(\\d+);`),
new RegExp(`export const ${constName}\\s*=\\s*([^;]+);`),
);
if (!match) {
throw new Error(`unable to read TypeScript const ${constName}`);
}
return Number(match[1]);
return evaluateNumberExpression(match[1]);
}
function evaluateNumberExpression(expression) {
const tokens = expression
.split('*')
.map((token) => token.trim())
.filter(Boolean);
if (
tokens.length === 0 ||
tokens.some((token) => !/^\d+$/.test(token))
) {
throw new Error(`unsupported numeric expression ${expression}`);
}
return tokens.reduce((value, token) => value * Number(token), 1);
}
function extractRustStringMatchArms(source, functionName) {
const match = source.match(
new RegExp(
`(?:pub\\(crate\\)\\s+)?fn ${functionName}[^\\{]*\\{[\\s\\S]*?match [^\\{]*\\{([\\s\\S]*?)\\n \\}`,
),
);
if (!match) {
throw new Error(`unable to read Rust match function ${functionName}`);
}
return [...match[1].matchAll(/"([^"]+)"\s*=>/g)].map((entry) => entry[1]);
}
function extractRustSomeStringValues(source, functionName) {
const match = source.match(
new RegExp(`(?:pub\\(crate\\)\\s+)?fn ${functionName}[^\\{]*\\{([\\s\\S]*?)\\n\\}`),
);
if (!match) {
throw new Error(`unable to read Rust function ${functionName}`);
}
return [...match[1].matchAll(/=>\s*Some\("([^"]+)"\)/g)].map(
(entry) => entry[1],
);
}
function extractDesktopCapabilities(source) {
@@ -891,6 +932,106 @@ const sharedHostBridgeVersion = extractTsNumberConst(
sharedContractSource,
'HOST_BRIDGE_VERSION',
);
const sharedHostBridgePayloadLimits = {
HOST_BRIDGE_REQUEST_ID_MAX_LENGTH: extractTsNumberConst(
sharedContractSource,
'HOST_BRIDGE_REQUEST_ID_MAX_LENGTH',
),
HOST_BRIDGE_BADGE_COUNT_MAX: extractTsNumberConst(
sharedContractSource,
'HOST_BRIDGE_BADGE_COUNT_MAX',
),
HOST_BRIDGE_CLIPBOARD_TEXT_MAX_LENGTH: extractTsNumberConst(
sharedContractSource,
'HOST_BRIDGE_CLIPBOARD_TEXT_MAX_LENGTH',
),
HOST_BRIDGE_LOCAL_NOTIFICATION_TITLE_MAX_LENGTH: extractTsNumberConst(
sharedContractSource,
'HOST_BRIDGE_LOCAL_NOTIFICATION_TITLE_MAX_LENGTH',
),
HOST_BRIDGE_LOCAL_NOTIFICATION_BODY_MAX_LENGTH: extractTsNumberConst(
sharedContractSource,
'HOST_BRIDGE_LOCAL_NOTIFICATION_BODY_MAX_LENGTH',
),
HOST_BRIDGE_FILE_NAME_MAX_LENGTH: extractTsNumberConst(
sharedContractSource,
'HOST_BRIDGE_FILE_NAME_MAX_LENGTH',
),
HOST_BRIDGE_EXPORT_TEXT_MAX_BYTES: extractTsNumberConst(
sharedContractSource,
'HOST_BRIDGE_EXPORT_TEXT_MAX_BYTES',
),
HOST_BRIDGE_IMPORT_TEXT_MAX_BYTES: extractTsNumberConst(
sharedContractSource,
'HOST_BRIDGE_IMPORT_TEXT_MAX_BYTES',
),
HOST_BRIDGE_EXPORT_IMAGE_MAX_BYTES: extractTsNumberConst(
sharedContractSource,
'HOST_BRIDGE_EXPORT_IMAGE_MAX_BYTES',
),
HOST_BRIDGE_IMPORT_IMAGE_MAX_BYTES: extractTsNumberConst(
sharedContractSource,
'HOST_BRIDGE_IMPORT_IMAGE_MAX_BYTES',
),
HOST_BRIDGE_EXPORT_AUDIO_MAX_BYTES: extractTsNumberConst(
sharedContractSource,
'HOST_BRIDGE_EXPORT_AUDIO_MAX_BYTES',
),
HOST_BRIDGE_IMPORT_AUDIO_MAX_BYTES: extractTsNumberConst(
sharedContractSource,
'HOST_BRIDGE_IMPORT_AUDIO_MAX_BYTES',
),
};
const desktopHostBridgePayloadLimits = {
HOST_BRIDGE_REQUEST_ID_MAX_LENGTH: extractRustNumberConst(
rustHostSource,
'HOST_BRIDGE_REQUEST_ID_MAX_LENGTH',
),
HOST_BRIDGE_BADGE_COUNT_MAX: extractRustNumberConst(
rustHostSource,
'BADGE_COUNT_MAX',
),
HOST_BRIDGE_CLIPBOARD_TEXT_MAX_LENGTH: extractRustNumberConst(
rustHostSource,
'CLIPBOARD_TEXT_MAX_LENGTH',
),
HOST_BRIDGE_LOCAL_NOTIFICATION_TITLE_MAX_LENGTH: extractRustNumberConst(
rustHostSource,
'LOCAL_NOTIFICATION_TITLE_MAX_LENGTH',
),
HOST_BRIDGE_LOCAL_NOTIFICATION_BODY_MAX_LENGTH: extractRustNumberConst(
rustHostSource,
'LOCAL_NOTIFICATION_BODY_MAX_LENGTH',
),
HOST_BRIDGE_FILE_NAME_MAX_LENGTH: extractRustNumberConst(
rustHostSource,
'EXPORT_FILE_NAME_MAX_LENGTH',
),
HOST_BRIDGE_EXPORT_TEXT_MAX_BYTES: extractRustNumberConst(
rustHostSource,
'EXPORT_TEXT_MAX_BYTES',
),
HOST_BRIDGE_IMPORT_TEXT_MAX_BYTES: extractRustNumberConst(
rustHostSource,
'IMPORT_TEXT_MAX_BYTES',
),
HOST_BRIDGE_EXPORT_IMAGE_MAX_BYTES: extractRustNumberConst(
rustHostSource,
'EXPORT_IMAGE_MAX_BYTES',
),
HOST_BRIDGE_IMPORT_IMAGE_MAX_BYTES: extractRustNumberConst(
rustHostSource,
'IMPORT_IMAGE_MAX_BYTES',
),
HOST_BRIDGE_EXPORT_AUDIO_MAX_BYTES: extractRustNumberConst(
rustHostSource,
'EXPORT_AUDIO_MAX_BYTES',
),
HOST_BRIDGE_IMPORT_AUDIO_MAX_BYTES: extractRustNumberConst(
rustHostSource,
'IMPORT_AUDIO_MAX_BYTES',
),
};
const desktopMethods = extractRustStringArrayConst(rustHostSource, 'HOST_BRIDGE_METHODS');
const desktopHostBridgeProtocol = extractRustStringConst(
rustHostSource,
@@ -919,6 +1060,62 @@ if (desktopHostBridgeVersion !== sharedHostBridgeVersion) {
);
}
for (const [limitName, sharedLimit] of Object.entries(
sharedHostBridgePayloadLimits,
)) {
const desktopLimit = desktopHostBridgePayloadLimits[limitName];
if (desktopLimit !== sharedLimit) {
throw new Error(
`desktop shell ${limitName} drifted: expected ${sharedLimit} but got ${desktopLimit}`,
);
}
}
if (
extractRustStringConst(rustHostSource, 'EXPORT_FILE_NAME_FALLBACK') !==
extractTsStringConst(sharedContractSource, 'HOST_BRIDGE_FILE_NAME_FALLBACK')
) {
throw new Error('desktop shell file name fallback drifted from shared HostBridge contract');
}
const sharedTextMimeTypes = extractStringArrayExport(
sharedContractSource,
'HOST_BRIDGE_TEXT_MIME_TYPES',
);
const sharedImageMimeTypes = extractStringArrayExport(
sharedContractSource,
'HOST_BRIDGE_IMAGE_MIME_TYPES',
);
const sharedAudioMimeTypes = extractStringArrayExport(
sharedContractSource,
'HOST_BRIDGE_AUDIO_MIME_TYPES',
);
assertSameList(
extractRustSomeStringValues(rustHostSource, 'import_text_mime_type'),
sharedTextMimeTypes,
'desktop shell text MIME types',
);
assertSameList(
extractRustStringMatchArms(rustHostSource, 'export_image_extension'),
sharedImageMimeTypes,
'desktop shell export image MIME types',
);
assertSameList(
extractRustSomeStringValues(rustHostSource, 'import_image_mime_type'),
sharedImageMimeTypes,
'desktop shell import image MIME types',
);
assertSameList(
extractRustStringMatchArms(rustHostSource, 'export_audio_extension'),
sharedAudioMimeTypes,
'desktop shell export audio MIME types',
);
assertSameList(
extractRustSomeStringValues(rustHostSource, 'import_audio_mime_type'),
sharedAudioMimeTypes,
'desktop shell import audio MIME types',
);
assertSameList(desktopMethods, sharedMethods, 'desktop shell HostBridge method whitelist');
const unknownHandledDesktopMethods = desktopHandledMethods.filter(
(method) => !sharedMethods.includes(method),