WIP: 游戏共创(作品 Fork):共创授权与改编血缘、创作族谱、取件与改编闭环 #622
+20
@@ -1767,6 +1767,7 @@ dependencies = [
|
||||
"jsonschema",
|
||||
"libc",
|
||||
"maud",
|
||||
"module-game-distribution",
|
||||
"nalgebra",
|
||||
"oxc_allocator",
|
||||
"oxc_ast",
|
||||
@@ -2890,6 +2891,17 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "module-game-distribution"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"hex",
|
||||
"serde",
|
||||
"sha2",
|
||||
"shared-kernel",
|
||||
"zip 2.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moxcms"
|
||||
version = "0.8.1"
|
||||
@@ -5000,6 +5012,14 @@ dependencies = [
|
||||
"ts-rs 12.0.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shared-kernel"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"time",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shared_library"
|
||||
version = "0.1.9"
|
||||
|
||||
@@ -82,6 +82,11 @@ zip = { version = "2", default-features = false, features = ["deflate"] }
|
||||
tauri-plugin-clipboard-manager = "2.3.2"
|
||||
maud = "0.27.0"
|
||||
|
||||
# P1-b 执行级交叉证据:客户端打包器产出的字节直接喂给服务端校验器
|
||||
# (`module-game-distribution::validate_project_bundle_zip`),替代只有文本级的一致性门禁。
|
||||
[dev-dependencies]
|
||||
module-game-distribution = { path = "../../../server-rs/crates/module-game-distribution" }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,11 @@ pub const MAX_PACKAGE_BYTES: u64 = 200 * 1024 * 1024;
|
||||
pub const MAX_EXPANDED_BYTES: u64 = 500 * 1024 * 1024;
|
||||
pub const MAX_FILE_BYTES: u64 = 64 * 1024 * 1024;
|
||||
pub const MAX_FILE_COUNT: usize = 10_000;
|
||||
/// 单条目压缩比上限:**声明解压大小 / 该条目自身的压缩字节** 的倍数。
|
||||
///
|
||||
/// 分母是该条目自己的压缩字节,不是整包体积:整包分母下,一个 10 MiB 的包单条可以声明
|
||||
/// 1 GiB 而不触发,这条检查几乎不生效。整包维度的主约束由 `MAX_PACKAGE_BYTES` /
|
||||
/// `MAX_FILE_BYTES` / `MAX_EXPANDED_BYTES` 承担。
|
||||
pub const MAX_COMPRESSION_RATIO: u64 = 100;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -74,7 +79,7 @@ pub fn validate_release_zip(bytes: &[u8]) -> Result<ReleasePackageManifest, Rele
|
||||
let mut expanded_bytes = 0_u64;
|
||||
let mut has_entry = false;
|
||||
for index in 0..archive.len() {
|
||||
let mut file = archive
|
||||
let file = archive
|
||||
.by_index(index)
|
||||
.map_err(|_| ReleasePackageError::InvalidArchive)?;
|
||||
if file.encrypted() {
|
||||
@@ -110,12 +115,23 @@ pub fn validate_release_zip(bytes: &[u8]) -> Result<ReleasePackageManifest, Rele
|
||||
if expanded_bytes > MAX_EXPANDED_BYTES {
|
||||
return Err(ReleasePackageError::ExpandedPackageTooLarge);
|
||||
}
|
||||
if declared_size > package_bytes.saturating_mul(MAX_COMPRESSION_RATIO) {
|
||||
if declared_size > file.compressed_size().saturating_mul(MAX_COMPRESSION_RATIO) {
|
||||
return Err(ReleasePackageError::CompressionRatioTooHigh);
|
||||
}
|
||||
|
||||
let mut content = Vec::with_capacity(usize::try_from(declared_size).unwrap_or(0));
|
||||
file.read_to_end(&mut content)
|
||||
// 读取层按**声明大小**封顶:`take(declared + 1)` 保证这条条目最多产出
|
||||
// `declared + 1` 字节——读满声明值再多 1 字节即判「声明说谎(少报展开大小)」,
|
||||
// 短读(实际不足声明值)同样失败。预分配收紧到单文件硬上限。
|
||||
//
|
||||
// 不变式:单条实际解压内存 ≤ 其声明大小 + 1(声明大小已被 `MAX_FILE_BYTES` 与
|
||||
// `MAX_EXPANDED_BYTES` 夹住),因此整包实际内存被两道上限约束,不会出现
|
||||
// 「声明 1 KiB、实际解压数 GiB」的内存放大。
|
||||
let mut reader = file.take(declared_size.saturating_add(1));
|
||||
let mut content = Vec::with_capacity(
|
||||
usize::try_from(declared_size.min(MAX_FILE_BYTES)).unwrap_or(usize::MAX),
|
||||
);
|
||||
reader
|
||||
.read_to_end(&mut content)
|
||||
.map_err(|_| ReleasePackageError::ReadFailed)?;
|
||||
if u64::try_from(content.len()).unwrap_or(u64::MAX) != declared_size {
|
||||
return Err(ReleasePackageError::ReadFailed);
|
||||
@@ -297,4 +313,54 @@ mod tests {
|
||||
assert_eq!(manifest.package_bytes, bytes.len() as u64);
|
||||
assert_eq!(manifest.files.len(), 3);
|
||||
}
|
||||
|
||||
/// deflate 零内容条目,用于低成本构造规模类用例。
|
||||
fn zeros_archive(path: &str, size: u64) -> Vec<u8> {
|
||||
let mut output = Cursor::new(Vec::new());
|
||||
let mut writer = ZipWriter::new(&mut output);
|
||||
writer
|
||||
.start_file(path, SimpleFileOptions::default())
|
||||
.expect("zip entry");
|
||||
let chunk = vec![0_u8; 1024 * 1024];
|
||||
let mut remaining = size;
|
||||
while remaining > 0 {
|
||||
let take = usize::try_from(remaining.min(chunk.len() as u64)).unwrap_or(chunk.len());
|
||||
writer.write_all(&chunk[..take]).expect("zip content");
|
||||
remaining -= take as u64;
|
||||
}
|
||||
writer.finish().expect("finish zip");
|
||||
output.into_inner()
|
||||
}
|
||||
|
||||
/// 把单条目 zip 声明的解压大小改成 `declared_size`,但不动实际 deflate 数据:
|
||||
/// 构造「声明说谎」的发行包(本地文件头与中央目录项一起改)。
|
||||
fn declared_size_lie_archive(path: &str, actual_size: u64, declared_size: u32) -> Vec<u8> {
|
||||
let mut bytes = zeros_archive(path, actual_size);
|
||||
assert_eq!(&bytes[0..4], b"PK\x03\x04", "local file header");
|
||||
// 本地文件头:… crc(14) + compressed(18) → 解压大小在偏移 22。
|
||||
bytes[22..26].copy_from_slice(&declared_size.to_le_bytes());
|
||||
let central = bytes
|
||||
.windows(4)
|
||||
.position(|window| window == b"PK\x01\x02")
|
||||
.expect("central directory header");
|
||||
// 中央目录项:… crc(16) + compressed(20) → 解压大小在偏移 24。
|
||||
bytes[central + 24..central + 28].copy_from_slice(&declared_size.to_le_bytes());
|
||||
bytes
|
||||
}
|
||||
|
||||
/// P0-b(发行包路径的回归证据):声明说谎必须在读取层失败关闭,不能先把 deflate
|
||||
/// 流整段解进内存再比对长度。
|
||||
#[test]
|
||||
fn rejects_entries_whose_declared_size_is_a_lie() {
|
||||
let under_report = declared_size_lie_archive("assets/liar.bin", 8 * 1024 * 1024, 1024);
|
||||
assert_eq!(
|
||||
validate_release_zip(&under_report),
|
||||
Err(ReleasePackageError::ReadFailed)
|
||||
);
|
||||
let over_report = declared_size_lie_archive("assets/liar.bin", 4096, 1500);
|
||||
assert_eq!(
|
||||
validate_release_zip(&over_report),
|
||||
Err(ReleasePackageError::ReadFailed)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user