Committed together: both streams landed in libs/game/script, so splitting them would produce two commits that don't compile. M6 — packaging and sharing - libs/zip_file gains a writer (store + deflate); real `unzip -t` validates our archives in an interop test. Packing is deterministic (fixed timestamps, sorted entries), so a package can be addressed by its own sha256 — which is what makes the registry's digest check mean anything - libs/game/pkg: .arcade format (game.splash + manifest.toml + assets), total manifest parsing (attacker bytes always yield a Manifest or an error, never a panic; non-finite numbers refused rather than defaulted), registry client that verifies sha256 INSIDE download so tampered bytes never reach the extractor - Hardened extraction: absolute paths, drive letters (C:x is absolute on Windows), UNC, backslashes, .., NUL/control chars, symlink members (via mode bits), duplicate names (the ambiguity IS the attack), declared-size caps checked before decompressing plus a post-decompress check, entry/ total/archive caps, and a post-join re-check that the resolved parent is still inside the destination — which catches a pre-existing symlink the name test cannot see. 4000-round mutation fuzz with a canary file beside the destination; a 320 MB deflate bomb under 1 MB on the wire is refused - Capability stripping rebinds fs/run/net to FRESH EMPTY OBJECTS rather than shadowing known verbs, so there is no hole the day someone adds one. Applied before the game handle is registered. Vacuity guard: an unstripped isolate genuinely reads a file, so the sandbox tests can't pass for unrelated reasons. Browser-installed games load Trust::Downloaded M7 — pretty pass - GameSun adopts draw::SceneSun (axis-converted: SceneSun is map-space y-south/z-up, games are y-up). Shaders compute hemisphere ambient + direct instead of each hardcoding its own split; defaults collapse the new formula to the old constants exactly, so unifying did not restyle existing games. write_into is the single write path — "one sun" is compiler-enforced - Projected shadow geometry: the caster's silhouette along the sun, fitted in the sun's own (u,v) frame, so it stretches as the sun swings. Nearest N casters get projection, the rest blobs; one instance in the existing alpha batch, no extra pass. 0.6us for 24 casters - Two pre-existing shadow bugs found via capture: the pipeline blends premultiplied, so unpremultiplied dark RGB ADDED light instead of removing it; and shadows were fogged, mixing them toward the bright horizon so a distant shadow came out lighter than the ground it darkened - Particles are structurally isolated from the sim: GameWorld has no particle field and step_world has no particle code — the renderer owns simulation and its own RNG. particles_never_advance_the_world_rng interleaves particle verbs with real rand() draws over 32 rounds and asserts both the RNG state and the drawn stream are identical - game.sfx_at with listener-relative gain/pan and a near-field ease so a sound at your feet doesn't flip channels; 2D verbs unchanged - apps/arcade/BUDGETS.md: measured particle/sim costs, Quest columns marked as estimates (the real particle limit is fill rate, not CPU) Tape probe BYTE_IDENTICAL. Not done: arcade has no audio backend, so positional sound is implemented and tested but not audible there yet. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
79 lines
2.7 KiB
Rust
79 lines
2.7 KiB
Rust
//! Interop: archives we write must satisfy a real zip implementation, not just
|
|
//! our own reader. Skips (rather than fails) where `unzip` is absent.
|
|
|
|
use makepad_zip_file::{ZipMethod, ZipWriter};
|
|
use std::process::Command;
|
|
|
|
fn have_unzip() -> bool {
|
|
Command::new("unzip")
|
|
.arg("-v")
|
|
.output()
|
|
.map(|o| o.status.success())
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
fn write_archive(path: &std::path::Path, method: ZipMethod) {
|
|
let mut w = ZipWriter::new();
|
|
w.add("game.splash", b"game.box({pos: vec3(0,1,0)})\n", method)
|
|
.unwrap();
|
|
w.add("manifest.toml", b"name = \"demo\"\nplayers_max = 4\n", method)
|
|
.unwrap();
|
|
w.add("assets/blob.bin", &vec![7u8; 50_000], method).unwrap();
|
|
w.add("nested/dir/deep.txt", b"deep", method).unwrap();
|
|
std::fs::write(path, w.finish().unwrap()).unwrap();
|
|
}
|
|
|
|
fn tmp_dir(tag: &str) -> std::path::PathBuf {
|
|
let dir = std::env::temp_dir().join(format!("makepad-zip-interop-{tag}-{}", std::process::id()));
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
dir
|
|
}
|
|
|
|
#[test]
|
|
fn system_unzip_validates_our_archives() {
|
|
if !have_unzip() {
|
|
eprintln!("skipping: `unzip` not on PATH");
|
|
return;
|
|
}
|
|
for (tag, method) in [("store", ZipMethod::Store), ("deflate", ZipMethod::Deflate)] {
|
|
let dir = tmp_dir(tag);
|
|
let zip = dir.join("pkg.arcade");
|
|
write_archive(&zip, method);
|
|
|
|
// -t verifies every member's CRC against its decompressed bytes, which
|
|
// is what catches a malformed central directory or a wrong crc/size.
|
|
let out = Command::new("unzip").arg("-t").arg(&zip).output().unwrap();
|
|
assert!(
|
|
out.status.success(),
|
|
"unzip -t failed for {tag}:\n{}\n{}",
|
|
String::from_utf8_lossy(&out.stdout),
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
|
|
// And the extracted bytes must actually match.
|
|
let dest = dir.join("out");
|
|
std::fs::create_dir_all(&dest).unwrap();
|
|
let out = Command::new("unzip")
|
|
.arg("-q")
|
|
.arg(&zip)
|
|
.arg("-d")
|
|
.arg(&dest)
|
|
.output()
|
|
.unwrap();
|
|
assert!(out.status.success(), "unzip extract failed for {tag}");
|
|
assert_eq!(
|
|
std::fs::read(dest.join("game.splash")).unwrap(),
|
|
b"game.box({pos: vec3(0,1,0)})\n"
|
|
);
|
|
assert_eq!(
|
|
std::fs::read(dest.join("assets/blob.bin")).unwrap(),
|
|
vec![7u8; 50_000]
|
|
);
|
|
assert_eq!(
|
|
std::fs::read(dest.join("nested/dir/deep.txt")).unwrap(),
|
|
b"deep"
|
|
);
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
}
|
|
}
|