Restore missing required asset/flow packages from the pinned nigig revision; retain upstream script isolation, X11 hover, and widget-core fixes. Validated makepad-widgets data feature with Rust 1.97.1.
241 lines
8.9 KiB
Rust
241 lines
8.9 KiB
Rust
//! Reference blobs over the wire: the real client publishing a real file it
|
|
//! never reads, against a real server on a real socket.
|
|
//!
|
|
//! What this proves that the core-level suite cannot:
|
|
//! - the route DOES NOT EXIST unless the embedder turned the policy on —
|
|
//! an untouched server answers 404, so no deployment gains a file-read
|
|
//! surface by upgrading,
|
|
//! - a bundle published by reference produces a manifest that a normal
|
|
//! client fetches normally: same digest, same length, same bytes,
|
|
//! - the store's directory gained no copy of the payload,
|
|
//! - and when the file underneath changes, the fetch REFUSES rather than
|
|
//! serving whatever is there now.
|
|
|
|
use makepad_asset_client::{
|
|
ApiEndpoints, AssetClient, ClientConfig, ClientError, PublishBundle, PublishBundleFile,
|
|
PublishRights, PublishThumbnail,
|
|
};
|
|
use makepad_asset_data::{AssetKind, BlobId, FileRole, MediaType, ThumbnailMedia};
|
|
use makepad_asset_store::{AssetServer, BlobRefPolicy, ServerConfig};
|
|
use std::path::{Path, PathBuf};
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
|
|
static DIR_COUNTER: AtomicU64 = AtomicU64::new(0);
|
|
|
|
fn test_root(name: &str) -> PathBuf {
|
|
let n = DIR_COUNTER.fetch_add(1, Ordering::Relaxed);
|
|
std::env::temp_dir().join(format!("mp_blobref_http_{}_{}_{}", std::process::id(), n, name))
|
|
}
|
|
|
|
fn start_server(name: &str, refs_on: bool) -> (AssetServer, String, PathBuf) {
|
|
let root = test_root(name);
|
|
let mut cfg = ServerConfig::new(root.clone());
|
|
cfg.control_addr = "127.0.0.1:0".parse().unwrap();
|
|
cfg.data_addr = "127.0.0.1:0".parse().unwrap();
|
|
cfg.bootstrap_admin = true;
|
|
cfg.log = false;
|
|
if refs_on {
|
|
cfg.blob_refs = BlobRefPolicy::local_host();
|
|
}
|
|
let server = AssetServer::start(cfg).expect("server start");
|
|
let token = std::fs::read_to_string(root.join("admin-token"))
|
|
.expect("admin token")
|
|
.trim()
|
|
.to_string();
|
|
(server, token, root)
|
|
}
|
|
|
|
fn connect(server: &AssetServer, token: &str, cache: &str) -> AssetClient {
|
|
let mut cfg = ClientConfig::new(test_root(cache));
|
|
cfg.token = Some(token.to_string());
|
|
let endpoints = ApiEndpoints { control: server.control_addr(), data: server.data_addr() };
|
|
AssetClient::connect(cfg, endpoints, Some(server.server_id())).expect("connect")
|
|
}
|
|
|
|
fn thumbnail() -> PublishThumbnail {
|
|
PublishThumbnail {
|
|
bytes: vec![9u8; 1200],
|
|
media: ThumbnailMedia::Jpeg,
|
|
width: 512,
|
|
height: 512,
|
|
views: Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// Count regular files under a directory — used to show the CAS did not grow.
|
|
fn file_count(root: &Path) -> usize {
|
|
let mut n = 0;
|
|
let mut stack = vec![root.to_path_buf()];
|
|
while let Some(dir) = stack.pop() {
|
|
let Ok(entries) = std::fs::read_dir(&dir) else { continue };
|
|
for entry in entries.flatten() {
|
|
let path = entry.path();
|
|
if path.is_dir() {
|
|
stack.push(path);
|
|
} else {
|
|
n += 1;
|
|
}
|
|
}
|
|
}
|
|
n
|
|
}
|
|
|
|
#[test]
|
|
fn reference_admission_is_absent_until_an_embedder_enables_it() {
|
|
let (server, token, _root) = start_server("policy_off", false);
|
|
let client = connect(&server, &token, "policy_off_cache");
|
|
let dir = test_root("policy_off_media");
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
let path = dir.join("clip.mp4");
|
|
std::fs::write(&path, b"UNREACHABLE").unwrap();
|
|
|
|
match client.admit_blob_ref("gen", path.to_str().unwrap()) {
|
|
Err(ClientError::NotFound { .. }) => {}
|
|
other => panic!("a server with the policy off must not offer the route: {other:?}"),
|
|
}
|
|
std::fs::remove_dir_all(&dir).ok();
|
|
}
|
|
|
|
#[test]
|
|
fn a_bundle_published_by_reference_fetches_like_any_other() {
|
|
let (server, token, root) = start_server("publish_ref", true);
|
|
let mut client = connect(&server, &token, "publish_ref_cache");
|
|
|
|
// The user's media directory, entirely outside the store.
|
|
let media = test_root("publish_ref_media");
|
|
std::fs::create_dir_all(&media).unwrap();
|
|
let clip: Vec<u8> = (0..40_000u32).map(|i| (i % 251) as u8).collect();
|
|
let path = media.join("opener.mp4");
|
|
std::fs::write(&path, &clip).unwrap();
|
|
|
|
let cas_before = file_count(&root.join("cas"));
|
|
|
|
let mut bundle = PublishBundle::new(
|
|
"gen",
|
|
AssetKind::Video,
|
|
"Opener",
|
|
vec![PublishBundleFile::reference(
|
|
FileRole::Video,
|
|
MediaType::Mp4,
|
|
path.clone(),
|
|
None,
|
|
)],
|
|
thumbnail(),
|
|
PublishRights::generated_cc0(),
|
|
);
|
|
bundle.media_millis = 4000;
|
|
let published = client.publish_bundle(&bundle).expect("publish by reference");
|
|
|
|
// The manifest pins the file's REAL digest and length, measured by the
|
|
// server on a file the client never opened.
|
|
assert_eq!(published.files.len(), 1);
|
|
assert_eq!(published.files[0].blob, BlobId::hash_of(&clip));
|
|
assert_eq!(published.files[0].byte_len, clip.len() as u64);
|
|
|
|
// The CAS gained exactly ONE object — the thumbnail. The 40 KB payload
|
|
// was not copied.
|
|
let cas_after = file_count(&root.join("cas"));
|
|
assert_eq!(
|
|
cas_after - cas_before,
|
|
1,
|
|
"the payload should not have been copied into the CAS"
|
|
);
|
|
|
|
// And it fetches over the wire like anything else, digest-verified by
|
|
// the client's own cache on the way in.
|
|
let fetched = client.fetch_blob_bytes(&published.files[0].blob, None).expect("fetch");
|
|
assert_eq!(fetched, clip);
|
|
|
|
// Break the file underneath: the store refuses rather than serving
|
|
// whatever is at that path now.
|
|
let mut tampered = clip.clone();
|
|
tampered[0] ^= 0xff;
|
|
std::fs::write(&path, &tampered).unwrap();
|
|
let mut fresh = connect(&server, &token, "publish_ref_cache2");
|
|
match fresh.fetch_blob_bytes(&published.files[0].blob, None) {
|
|
Ok(_) => panic!("a drifted reference must never serve"),
|
|
Err(_) => {}
|
|
}
|
|
|
|
// Restore, and it works again: the reference was unavailable, not wrong.
|
|
std::fs::write(&path, &clip).unwrap();
|
|
let mut again = connect(&server, &token, "publish_ref_cache3");
|
|
assert_eq!(
|
|
again.fetch_blob_bytes(&published.files[0].blob, None).expect("restored"),
|
|
clip
|
|
);
|
|
|
|
std::fs::remove_dir_all(&media).ok();
|
|
}
|
|
|
|
#[test]
|
|
fn the_prefix_allowlist_is_enforced() {
|
|
let root = test_root("allowlist");
|
|
let allowed = test_root("allowlist_allowed");
|
|
let forbidden = test_root("allowlist_forbidden");
|
|
std::fs::create_dir_all(&allowed).unwrap();
|
|
std::fs::create_dir_all(&forbidden).unwrap();
|
|
std::fs::write(allowed.join("ok.mp4"), b"INSIDE").unwrap();
|
|
std::fs::write(forbidden.join("no.mp4"), b"OUTSIDE").unwrap();
|
|
|
|
let mut cfg = ServerConfig::new(root.clone());
|
|
cfg.control_addr = "127.0.0.1:0".parse().unwrap();
|
|
cfg.data_addr = "127.0.0.1:0".parse().unwrap();
|
|
cfg.bootstrap_admin = true;
|
|
cfg.log = false;
|
|
cfg.blob_refs = BlobRefPolicy { roots: vec![allowed.clone()], ..BlobRefPolicy::local_host() };
|
|
let server = AssetServer::start(cfg).expect("server start");
|
|
let token = std::fs::read_to_string(root.join("admin-token")).unwrap().trim().to_string();
|
|
let client = connect(&server, &token, "allowlist_cache");
|
|
|
|
client
|
|
.admit_blob_ref("gen", allowed.join("ok.mp4").to_str().unwrap())
|
|
.expect("a path under an allowed root is admitted");
|
|
|
|
assert!(
|
|
client
|
|
.admit_blob_ref("gen", forbidden.join("no.mp4").to_str().unwrap())
|
|
.is_err(),
|
|
"a path outside every allowed root must be refused"
|
|
);
|
|
|
|
// …including one that tries to walk out of an allowed root.
|
|
let escape = allowed.join("..").join("allowlist_forbidden").join("no.mp4");
|
|
assert!(
|
|
client.admit_blob_ref("gen", escape.to_str().unwrap()).is_err(),
|
|
"a traversal out of an allowed root must be refused"
|
|
);
|
|
|
|
std::fs::remove_dir_all(&allowed).ok();
|
|
std::fs::remove_dir_all(&forbidden).ok();
|
|
}
|
|
|
|
#[test]
|
|
fn rescan_over_http_reports_what_went_stale() {
|
|
let (server, token, _root) = start_server("rescan_http", true);
|
|
let client = connect(&server, &token, "rescan_http_cache");
|
|
let media = test_root("rescan_http_media");
|
|
std::fs::create_dir_all(&media).unwrap();
|
|
let good = media.join("good.mp4");
|
|
let gone = media.join("gone.mp4");
|
|
std::fs::write(&good, b"STILL-HERE").unwrap();
|
|
std::fs::write(&gone, b"NOT-FOR-LONG").unwrap();
|
|
client.admit_blob_ref("gen", good.to_str().unwrap()).unwrap();
|
|
client.admit_blob_ref("gen", gone.to_str().unwrap()).unwrap();
|
|
std::fs::remove_file(&gone).unwrap();
|
|
|
|
let page = client.blob_refs_page(None, 32).expect("rescan");
|
|
assert_eq!(page.total, 2);
|
|
assert_eq!(page.refs.len(), 2);
|
|
let state_of = |name: &str| {
|
|
page.refs
|
|
.iter()
|
|
.find(|r| r.path.ends_with(name))
|
|
.map(|r| r.state.clone())
|
|
.unwrap_or_default()
|
|
};
|
|
assert_eq!(state_of("good.mp4"), "present");
|
|
assert_eq!(state_of("gone.mp4"), "missing");
|
|
|
|
std::fs::remove_dir_all(&media).ok();
|
|
}
|