WIP: 游戏共创(作品 Fork):共创授权与改编血缘、创作族谱、取件与改编闭环 #622

Draft
suzmii wants to merge 131 commits from feat/game-fork into master
5 changed files with 1060 additions and 66 deletions
Showing only changes of commit 10d1fcbc5f - Show all commits
+20
View File
@@ -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