陶泥儿产物按平台发行包上限预检

- build.rs 保留 512 MiB 读取安全线,另按 shared-contracts 的 GAME_DISTRIBUTION_MAX_PACKAGE_BYTES(200 MiB)做预检
- 构建收尾与发布读包两处都预检,超限给「精简资源后重新导出再发布」,不再等上传吃服务端 413
- 新增平台上限单测,并锁定读取安全线必须高于平台上限
This commit is contained in:
2026-10-07 01:26:24 +08:00
parent d28b178c04
commit afbcc79978
@@ -35,8 +35,24 @@ const OUTPUT_TAIL_MAX_CHARS: usize = 4_000;
const ARTIFACT_ENTRY_FILE_NAME: &str = "index.html";
/// 读取产物时单次读入的字节数。sha256 与字节缓冲共用这一遍流式读取。
const PACKAGE_READ_CHUNK_BYTES: usize = 64 * 1024;
/// 产物 zip 的合理上限:与试玩包同一口径(512 MiB),超过它直接拒绝读入内存。
const ARTIFACT_PACKAGE_MAX_BYTES: u64 = 512 * 1024 * 1024;
/// 读取产物时的体积安全线:超过它直接拒绝读入内存(与旧本地发行包读取口径一致)。
const ARTIFACT_PACKAGE_READ_MAX_BYTES: u64 = 512 * 1024 * 1024;
/// 平台发行包上限(200 MiB):单一来源是 `shared-contracts`,这里只做发布前的同口径预检。
///
/// 与上一条读取安全线不是一个概念:安全线管「能不能读进内存」,这里管「平台上能不能收」。
/// 构建收尾与发布读包两处都过它,避免作者等完一轮再吃服务端 413。
fn ensure_within_platform_package_limit(size_bytes: u64) -> Result<(), String> {
let limit = shared_contracts::game_distribution::GAME_DISTRIBUTION_MAX_PACKAGE_BYTES;
if size_bytes > limit {
return Err(format!(
"发行包 {:.1} MiB 超过平台上限 {} MiB;请精简资源后重新导出再发布",
size_bytes as f64 / (1024.0 * 1024.0),
limit / (1024 * 1024)
));
}
Ok(())
}
/// 固定产物读出来的包:发布链路只需要这几项事实,不需要逐条文件摘要。
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -164,12 +180,14 @@ fn conclude(
&& !metadata.file_type().is_symlink()
&& metadata.len() > 0
);
// zip 规范:根目录必须有 `index.html`。脚本"成功"但产物不是合规 zip(或写到了别处),
// 与没有产物同类,都报 artifactMissing。
// zip 规范:根目录必须有 `index.html`,且包体不得超过平台发行包上限。脚本"成功"但产物
// 不合规(或写到了别处),与没有产物同类,都报 artifactMissing。
let bytes = std::fs::read(&path).ok();
let conformant = produced
&& std::fs::read(&path)
.map(|bytes| inspect_artifact(&bytes).is_ok())
.unwrap_or(false);
&& bytes.as_deref().is_some_and(|bytes| {
ensure_within_platform_package_limit(bytes.len() as u64).is_ok()
&& inspect_artifact(bytes).is_ok()
});
if !conformant {
return Err(TaonierExportBuildError::ArtifactMissing(
TaonierArtifactMissing {
@@ -201,13 +219,14 @@ pub(crate) fn read_artifact_package(root: &Path) -> Result<TaonierArtifactPackag
if metadata.len() == 0 {
return Err("陶泥儿产物为空".to_string());
}
if metadata.len() > ARTIFACT_PACKAGE_MAX_BYTES {
if metadata.len() > ARTIFACT_PACKAGE_READ_MAX_BYTES {
return Err(format!(
"陶泥儿产物大小超出上限:{}B > {}B",
metadata.len(),
ARTIFACT_PACKAGE_MAX_BYTES
ARTIFACT_PACKAGE_READ_MAX_BYTES
));
}
ensure_within_platform_package_limit(metadata.len())?;
let (package_bytes, package_sha256) = read_package_bytes(&path)?;
if package_bytes.len() as u64 != metadata.len() {
@@ -240,10 +259,10 @@ fn read_package_bytes(path: &Path) -> Result<(Vec<u8>, String), String> {
}
hasher.update(&buffer[..read]);
bytes.extend_from_slice(&buffer[..read]);
if bytes.len() as u64 > ARTIFACT_PACKAGE_MAX_BYTES {
if bytes.len() as u64 > ARTIFACT_PACKAGE_READ_MAX_BYTES {
return Err(format!(
"陶泥儿产物大小超出上限:{}B",
ARTIFACT_PACKAGE_MAX_BYTES
ARTIFACT_PACKAGE_READ_MAX_BYTES
));
}
}
@@ -489,6 +508,21 @@ mod tests {
assert_eq!(unavailable.cause, "命令沙箱不可用");
}
#[test]
fn platform_package_limit_is_the_shared_contract_and_reports_mib() {
let limit = shared_contracts::game_distribution::GAME_DISTRIBUTION_MAX_PACKAGE_BYTES;
assert!(ensure_within_platform_package_limit(limit).is_ok());
// 读取安全线(512 MiB)与平台上限(200 MiB)不是一个概念:这里必须卡平台上限。
assert!(limit < ARTIFACT_PACKAGE_READ_MAX_BYTES);
let error =
ensure_within_platform_package_limit(limit + 1).expect_err("超过平台上限必须失败");
assert!(
error.contains("超过平台上限"),
"错误文案应说明超限:{error}"
);
assert!(error.contains("200 MiB"), "错误文案应带上平台上限:{error}");
}
#[test]
fn reads_the_fixed_artifact_with_hash_size_and_file_count() {
let root = fixture_root("read-package");