Something went wrong. Try again.
atproto Thingiverse but good
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874use axum::body::Body;use axum::extract::{Path as AxumPath, State};use axum::http::{StatusCode, header};use axum::response::Response;use base64::Engine as _;use futures::TryStreamExt;use futures::stream;use jacquard::identity::resolver::IdentityResolver;use jacquard_axum::ExtractXrpc;use jacquard_common::types::string::{AtUri, Cid, Did};use jacquard_common::xrpc::XrpcExt;use polymodel_api::com_atproto::sync::get_blob::GetBlob;use polymodel_api::space_polymodel::library::get_model_load_bundle::GetModelLoadBundleRequest;use polymodel_renderer_protocol::{ CacheKey as ProtocolCacheKey, MAX_MODEL_METADATA_BYTES, MAX_MODEL_RESOURCE_BYTES, MAX_MODEL_RESOURCES, MeshFormat, ModelLoadBundle, ModelResource, PROTOCOL_VERSION, RootId as ProtocolRootId, Span as ProtocolSpan, TargetSelection,};use serde::Deserialize;#[cfg(test)]use serde::Serialize;use sha2::{Digest, Sha256};use std::collections::{HashMap, HashSet, VecDeque};use std::path::{Path, PathBuf};
use super::error::{AppResult, db, internal, invalid_request, not_found, verify_chunk_cid};use super::state::AppState;use crate::ldraw::resolve::{ResolvedResource, SqliteResolver};use crate::ldraw::{CanonicalPath, LdrawResourceResolver, RootId};
const ROUTE_PREFIX: &str = "/app/ldraw/resources/";const ROUTE_VERSION: u8 = 1;const MAX_MANIFEST_EDGES: usize = MAX_MODEL_RESOURCES * 8;
#[derive(Debug)]struct ManifestEntry { root: RootId, path: CanonicalPath, target_resource_uri: String, target_cid: String, byte_length: i64, sha256: Vec<u8>, mime_type: String,}
#[derive(Debug, Deserialize)]pub(super) struct ModelBundleRequest { pub uri: AtUri,}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]pub(super) struct RouteBinding { version: u8, project: AtUri, publisher: Did, source: AtUri, root: RootId, path: CanonicalPath, target_cids: Vec<String>, byte_length: u64, sha256: Vec<u8>,}
#[cfg(test)]#[derive(Debug, Serialize)]struct RoutePayload<'a> { v: u8, project: &'a AtUri, publisher: &'a Did, source: &'a AtUri, root: &'a str, path: &'a str, cids: &'a [String], len: u64, sha256: &'a [u8],}
#[derive(Debug, Deserialize)]struct RoutePayloadOwned { v: u8, project: AtUri, publisher: Did, source: AtUri, root: String, path: String, cids: Vec<String>, len: u64, sha256: Vec<u8>,}
pub(super) async fn get_model_load_bundle( State(state): State<AppState>, ExtractXrpc(request): ExtractXrpc<GetModelLoadBundleRequest>,) -> AppResult<Response> { build_model_load_bundle(state, request.uri).await}
pub(super) async fn app_model_load_bundle( State(state): State<AppState>, axum::Json(request): axum::Json<ModelBundleRequest>,) -> AppResult<Response> { build_model_load_bundle(state, request.uri).await}
fn child_root_local(node: &BundleNode) -> RootId { match node { BundleNode::Db(resolved) => resolved.root, BundleNode::Official(official) => official.root, }}
fn cached_target_key( cache: &HashMap<(RootId, CanonicalPath), ProtocolCacheKey>, preferred_root: RootId, path: &CanonicalPath,) -> Option<ProtocolCacheKey> { cache .get(&(preferred_root, path.clone())) .or_else(|| { path.as_str() .split('/') .next() .and_then(parse_root) .and_then(|root| cache.get(&(root, path.clone()))) }) .cloned()}
async fn load_bundle_root(state: &AppState, uri: &AtUri) -> AppResult<(AtUri, CanonicalPath)> { let Some(row) = db(sqlx::query!( "SELECT project_uri, canonical_path FROM ldraw_resources WHERE resource_uri = ?", uri.as_ref(), ) .fetch_optional(&state.pool) .await)? else { tracing::warn!(%uri, stage = "root_projection_miss", "compound model-load bundle root was not projected"); return Err(not_found()); }; let project = AtUri::new_owned(row.project_uri).map_err(|_| { tracing::error!(%uri, stage = "root_project_uri_invalid", "compound model-load bundle root has an invalid project URI"); not_found() })?; let path = CanonicalPath::parse(&row.canonical_path).map_err(|_| { tracing::error!(%uri, stage = "root_path_invalid", canonical_path = %row.canonical_path, "compound model-load bundle root has an invalid canonical path"); not_found() })?; Ok((project, path))}
/// A resolved LDraw resource in a model-load bundle. DB-backed resources are/// fetched from the user's PDS via blob CIDs; official-library resources are/// read from the local filesystem fallback and carry inline bytes.#[derive(Debug, Clone)]enum BundleNode { Db(Box<ResolvedResource>), Official(OfficialResource),}
/// A resource read from the local official LDraw library directory. Unlike/// `ResolvedResource`, bytes are inline in the bundle (no PDS fetch, no blob/// CIDs, no route binding). The SHA-256 is computed at read time and verified/// client-side by `ModelResource::validate()`.#[derive(Debug, Clone)]struct OfficialResource { root: RootId, path: CanonicalPath, bytes: Vec<u8>, sha256: Vec<u8>, mime_type: String,}
/// Resolve a bundle node: try the DB-backed resolver first, then probe the/// official LDraw library using the shared permissive candidate policy.async fn resolve_bundle_node( resolver: &SqliteResolver, project: &AtUri, path: &CanonicalPath, libdir: Option<&PathBuf>,) -> AppResult<BundleNode> { tracing::debug!(path = %path, "resolving compound bundle path"); match resolver.resolve(project, path).await { Ok(resolved) => { tracing::debug!(path = %path, source = "projection", "resolved compound bundle path"); Ok(BundleNode::Db(Box::new(resolved))) } Err(crate::ldraw::ResolveError::NotFound) => { tracing::debug!(path = %path, source = "projection", "compound bundle projection miss"); let Some(libdir) = libdir else { return Err(resolve_error(crate::ldraw::ResolveError::NotFound)); }; if path.segments().next().is_some_and(|root| { !root.eq_ignore_ascii_case("parts") && !root.eq_ignore_ascii_case("p") }) { tracing::warn!(path = %path, "probing non-standard logical path in official LDraw library"); } let official = resolve_official_library(path, libdir).await; tracing::debug!( path = %path, source = "official-library", found = official.is_some(), "resolved compound bundle official-library fallback" ); official.map(BundleNode::Official).ok_or_else(not_found) } Err(error) => Err(resolve_error(error)), }}
/// Read an official LDraw library file from the filesystem using the shared/// candidate policy. Case-insensitive matching is used for every path segment.async fn resolve_official_library(path: &CanonicalPath, libdir: &Path) -> Option<OfficialResource> { let normalized = polymodel_ldraw_core::NormalizedPath::new(path.as_str()).ok()?; let candidates = polymodel_ldraw_core::official_library_candidates(&normalized).ok()?; let (logical_path, file_path) = find_official_candidate(libdir, &candidates).await?; let root = if logical_path.as_str().starts_with("p/") { RootId::OfficialP } else { RootId::OfficialParts }; read_official_resource( root, logical_path.as_str(), &file_path, "application/x-ldraw", ) .await}
/// Resolve a TEXMAP asset or the shared colour table from the official library./// Bare TEXMAP filenames are rooted at `textures/`; explicit `textures/` paths/// remain canonical. No path outside the configured library is accepted.async fn resolve_official_asset(reference: &str, libdir: &Path) -> Option<OfficialResource> { let normalized = polymodel_ldraw_core::NormalizedPath::new(reference).ok()?; let canonical_root = tokio::fs::canonicalize(libdir).await.ok()?; if normalized.as_str().eq_ignore_ascii_case("ldconfig.ldr") { let file_path = case_insensitive_lookup(libdir, "LDConfig.ldr").await?; if !tokio::fs::canonicalize(&file_path) .await .ok()? .starts_with(&canonical_root) { return None; } return read_official_resource( RootId::OfficialParts, "LDConfig.ldr", &file_path, "application/x-ldraw", ) .await; } let relative = normalized .as_str() .strip_prefix("textures/") .unwrap_or(normalized.as_str()); if relative.is_empty() || relative.eq_ignore_ascii_case(".") { return None; } let textures_dir = case_insensitive_lookup(libdir, "textures").await?; let mut current = textures_dir; for segment in relative.split('/') { current = case_insensitive_lookup(¤t, segment).await?; } let logical_path = format!("textures/{relative}"); if !tokio::fs::canonicalize(¤t) .await .ok()? .starts_with(&canonical_root) { return None; } let mime_type = match logical_path .rsplit_once('.') .map(|(_, extension)| extension.to_ascii_lowercase()) .as_deref() { Some("png") => "image/png", Some("jpg" | "jpeg") => "image/jpeg", Some("webp") => "image/webp", _ => "application/octet-stream", }; read_official_resource(RootId::OfficialParts, &logical_path, ¤t, mime_type).await}
async fn read_official_resource( root: RootId, logical_path: &str, file_path: &Path, mime_type: &str,) -> Option<OfficialResource> { let metadata = tokio::fs::metadata(file_path).await.ok()?; let file_size = metadata.len(); if file_size > MAX_MODEL_RESOURCE_BYTES || !metadata.is_file() { return None; } let bytes = tokio::fs::read(file_path).await.ok()?; if bytes.len() as u64 != file_size { return None; } Some(OfficialResource { root, path: CanonicalPath::parse(logical_path).ok()?, bytes: bytes.clone(), sha256: Sha256::digest(&bytes).to_vec(), mime_type: mime_type.to_owned(), })}
async fn find_official_candidate( libdir: &Path, candidates: &[polymodel_ldraw_core::NormalizedPath],) -> Option<(polymodel_ldraw_core::NormalizedPath, PathBuf)> { let canonical_root = tokio::fs::canonicalize(libdir).await.ok()?; for candidate in candidates { let mut segments = candidate.as_str().split('/'); let root = segments.next()?; if !root.eq_ignore_ascii_case("parts") && !root.eq_ignore_ascii_case("p") { continue; } let Some(mut current) = case_insensitive_lookup(libdir, root).await else { continue; }; let mut found = true; for segment in segments { let Some(next) = case_insensitive_lookup(¤t, segment).await else { found = false; break; }; current = next; } if !found { continue; } let Ok(canonical) = tokio::fs::canonicalize(¤t).await else { continue; }; if canonical.starts_with(&canonical_root) && canonical.is_file() { return Some((candidate.clone(), current)); } } None}
/// Look up one path segment in a directory case-insensitively. Exact matches/// win, while the fallback handles official files authored with loose casing.async fn case_insensitive_lookup(dir: &Path, name: &str) -> Option<PathBuf> { let exact = dir.join(name); if tokio::fs::metadata(&exact).await.is_ok() { return Some(exact); } let mut entries = tokio::fs::read_dir(dir).await.ok()?; while let Ok(Some(entry)) = entries.next_entry().await { if entry .file_name() .to_str() .is_some_and(|n| n.eq_ignore_ascii_case(name)) { return Some(entry.path()); } } None}
async fn resolve_child_resource( state: &AppState, resolver: &SqliteResolver, project: &AtUri, parent: &BundleNode, child_path: &CanonicalPath, manifest: &[ManifestEntry], libdir: Option<&PathBuf>,) -> AppResult<(BundleNode, Vec<u8>)> { let child = if let BundleNode::Db(resolved) = parent { match resolver.resolve(project, child_path).await { Ok(child) => { validate_manifest_child(resolved, &child, manifest)?; let child_bytes = fetch_verified_bytes( state, &child, u64::try_from(child.byte_length) .map_err(|_| invalid_request("negative child length"))?, ) .await?; (BundleNode::Db(Box::new(child)), child_bytes) } Err(crate::ldraw::ResolveError::NotFound) => { let official = resolve_official_library(child_path, libdir.ok_or_else(not_found)?) .await .ok_or_else(not_found)?; (BundleNode::Official(official.clone()), official.bytes) } Err(error) => return Err(resolve_error(error)), } } else { let official = resolve_official_library(child_path, libdir.ok_or_else(not_found)?) .await .ok_or_else(not_found)?; (BundleNode::Official(official.clone()), official.bytes) }; Ok(child)}
async fn build_model_load_bundle(state: AppState, uri: AtUri) -> AppResult<Response> { let (project, path) = load_bundle_root(&state, &uri).await?; let resolver = SqliteResolver::new(state.pool.clone()); let libdir = state.ldraw_library_dir.as_deref(); tracing::info!( %uri, root = %path, library_configured = libdir.is_some(), "building compound LDraw model-load bundle" ); let mut resources: Vec<(BundleNode, Vec<u8>)> = Vec::new(); let mut target_keys_by_path: HashMap<(RootId, CanonicalPath), ProtocolCacheKey> = HashMap::new(); let mut resolved_identities: HashSet<(ProtocolRootId, polymodel_ldraw_core::NormalizedPath)> = HashSet::new(); let root_node = resolve_bundle_node(&resolver, &project, &path, libdir).await?; let root_bytes = match &root_node { BundleNode::Db(resolved) => { let binding = binding_for(&project, resolved)?; fetch_verified_bytes(&state, resolved, binding.byte_length).await? } BundleNode::Official(official) => official.bytes.clone(), }; let root_root = match &root_node { BundleNode::Db(resolved) => resolved.root, BundleNode::Official(official) => official.root, }; let root_protocol_root = protocol_root(root_root); let root_key = ProtocolCacheKey::new( polymodel_renderer_protocol::NormalizedPath::new(path.as_str()) .map_err(|_| invalid_request("invalid bundled root path"))?, root_protocol_root, &root_bytes, ); target_keys_by_path.insert((root_root, path.clone()), root_key.clone()); let mut queue = VecDeque::from([(path.clone(), root_node, root_bytes, root_key)]); let mut source_identities = Vec::new(); let mut target_selections = Vec::new(); let mut resolved_closure = polymodel_ldraw_core::ResourceClosure { include_references: Vec::new(), dependencies: Vec::new(), resolved: Vec::new(), needs_ldconfig: false, }; let mut manifest_edges = 0_usize; let mut total_bytes = 0_u64;
while let Some((current_path, node, bytes, source_key)) = queue.pop_front() { if resources.len() >= MAX_MODEL_RESOURCES { return Err(invalid_request( "compound resource descriptor bound exceeded", )); } let current_root = match &node { BundleNode::Db(resolved) => resolved.root, BundleNode::Official(official) => official.root, }; let current_protocol_root = protocol_root(current_root); let projected_length = u64::try_from(bytes.len()) .map_err(|_| invalid_request("compound resource byte length overflow"))?; let remaining_budget = MAX_MODEL_RESOURCE_BYTES .checked_sub(total_bytes) .ok_or_else(|| invalid_request("compound byte bound overflow"))?; if projected_length > remaining_budget { return Err(invalid_request("compound plan bounds exceeded")); } total_bytes = total_bytes .checked_add(projected_length) .ok_or_else(|| invalid_request("compound byte bound overflow"))?; let inspection = polymodel_ldraw_core::inspect_compound_source( &bytes, &polymodel_renderer_protocol::NormalizedPath::new(current_path.as_str()) .map_err(|_| invalid_request("invalid bundled source path"))?, current_protocol_root, ) .map_err(|_| not_found())?; let mut inspection = inspection; let source_keys = std::mem::take(&mut inspection.source_keys); let has_virtual_files = inspection.has_virtual_files; if has_virtual_files { for (_, identity) in &source_keys { if !source_identities.contains(identity) { source_identities.push(identity.clone()); } } } let resource_closure = polymodel_ldraw_core::ResourceClosure::from_inspection( &mut inspection, &polymodel_renderer_protocol::NormalizedPath::new(current_path.as_str()) .map_err(|_| invalid_request("invalid bundled source path"))?, current_protocol_root, ); let mut auxiliary_dependencies = resource_closure .texture_dependencies() .cloned() .collect::<Vec<_>>(); let needs_ldconfig = resource_closure.needs_ldconfig; if needs_ldconfig { auxiliary_dependencies.push(polymodel_ldraw_core::ResourceDependency { kind: polymodel_ldraw_core::ResourceKind::LdConfig, path: polymodel_ldraw_core::NormalizedPath::new("LDConfig.ldr") .map_err(|_| invalid_request("invalid LDConfig path"))?, source: polymodel_ldraw_core::NormalizedPath::new(current_path.as_str()) .map_err(|_| invalid_request("invalid bundled source path"))?, root: polymodel_ldraw_core::RootId::OfficialLibrary, required: false, fallback_available: true, }); } resolved_closure .dependencies .extend(resource_closure.dependencies.iter().cloned()); resolved_closure.needs_ldconfig |= needs_ldconfig; let references = resource_closure.include_references; let source = match &node { BundleNode::Db(_) => "projection", BundleNode::Official(_) => "official-library", }; let reachable_sections = reachable_mpd_sections(¤t_path, &references); let eligible_references = references .iter() .filter(|reference| { !reference.target_virtual_file && reachable_sections.contains(&reference.file_ordinal) }) .count(); let virtual_target_references = references .iter() .filter(|reference| reference.target_virtual_file) .count(); let other_section_references = references .iter() .filter(|reference| { !reference.target_virtual_file && !reachable_sections.contains(&reference.file_ordinal) }) .count(); let reference_names = references .iter() .filter(|reference| { !reference.target_virtual_file && reachable_sections.contains(&reference.file_ordinal) }) .take(12) .map(|reference| reference.target.as_str()) .collect::<Vec<_>>(); tracing::info!( path = %current_path, source, bytes = bytes.len(), extracted_references = references.len(), eligible_references, virtual_target_references, other_section_references, reference_names = ?reference_names, "resolved compound bundle resource" );
// Manifest rows only exist for DB-backed project resources. let manifest = if let BundleNode::Db(resolved) = &node { let rows = db( sqlx::query!( "SELECT root, canonical_path, target_resource_uri, target_cid, byte_length, sha256, mime_type, ordinal FROM ldraw_manifest_files WHERE resource_uri = ? ORDER BY ordinal ASC", resolved.resource.uri.as_ref(), ) .fetch_all(&state.pool) .await, )?; rows.into_iter() .map(|row| { Ok::<_, super::error::AppError>(ManifestEntry { root: parse_root(&row.root).ok_or_else(not_found)?, path: CanonicalPath::parse(&row.canonical_path).map_err(|_| not_found())?, target_resource_uri: row.target_resource_uri, target_cid: row.target_cid, byte_length: row.byte_length, sha256: row.sha256, mime_type: row.mime_type, }) }) .collect::<AppResult<Vec<_>>>()? } else { Vec::new() };
let mut children = Vec::with_capacity(manifest.len()); for reference in references.iter().filter(|reference| { !reference.target_virtual_file && reachable_sections.contains(&reference.file_ordinal) }) { manifest_edges = manifest_edges .checked_add(1) .ok_or_else(|| invalid_request("compound manifest edge bound overflow"))?; if manifest_edges > MAX_MANIFEST_EDGES { return Err(invalid_request("compound manifest edge bound exceeded")); } let child_path = core_reference_path(¤t_path, reference).ok_or_else(not_found)?; let source_key = if has_virtual_files { source_keys .iter() .find(|(name, _)| name == &reference.file_name.to_ascii_lowercase()) .map(|(_, key)| key.clone()) .ok_or_else(not_found)? } else { source_key.clone() }; let child_key = if let Some(child_key) = cached_target_key(&target_keys_by_path, current_root, &child_path) { child_key } else { // Resolve, read, and key a new child before queueing it. Duplicate // references take the cache-key branch above and do no I/O. let child = resolve_child_resource( &state, &resolver, &project, &node, &child_path, &manifest, libdir, ) .await?; let requested_child_path = child_path.clone(); let child_path = match &child.0 { BundleNode::Db(resolved) => resolved.path.clone(), BundleNode::Official(official) => official.path.clone(), }; let child_root = match &child.0 { BundleNode::Db(resolved) => protocol_root(resolved.root), BundleNode::Official(official) => protocol_root(official.root), }; let child_key = ProtocolCacheKey::new( polymodel_renderer_protocol::NormalizedPath::new(child_path.as_str()) .map_err(|_| invalid_request("invalid child path"))?, child_root, &child.1, ); target_keys_by_path.insert((current_root, requested_child_path), child_key.clone()); target_keys_by_path.insert( (child_root_local(&child.0), child_path.clone()), child_key.clone(), ); children.push((child_path, child.0, child.1, child_key.clone())); child_key }; target_selections.push(TargetSelection { source: source_key, span: ProtocolSpan { start: reference.span.start, end: reference.span.end, line: reference.span.line, column: reference.span.column, }, target: child_key, }); } // Manifest-only companions are enqueued with their bytes so the BFS // never performs a second resolver/fetch pass. for entry in &manifest { if cached_target_key(&target_keys_by_path, entry.root, &entry.path).is_none() { manifest_edges = manifest_edges .checked_add(1) .ok_or_else(|| invalid_request("compound manifest edge bound overflow"))?; if manifest_edges > MAX_MANIFEST_EDGES { return Err(invalid_request("compound manifest edge bound exceeded")); } let Some(resolved) = (match &node { BundleNode::Db(_) => match resolver.resolve(&project, &entry.path).await { Ok(child) => Some(BundleNode::Db(Box::new(child))), Err(crate::ldraw::ResolveError::NotFound) if libdir.is_some() => { resolve_official_library(&entry.path, libdir.unwrap()) .await .map(BundleNode::Official) } Err(error) => return Err(resolve_error(error)), }, BundleNode::Official(_) => None, }) else { continue; }; let child_bytes = match &resolved { BundleNode::Db(child) => { fetch_verified_bytes( &state, child, u64::try_from(child.byte_length) .map_err(|_| invalid_request("negative child length"))?, ) .await? } BundleNode::Official(child) => child.bytes.clone(), }; let resolved_path = match &resolved { BundleNode::Db(child) => child.path.clone(), BundleNode::Official(child) => child.path.clone(), }; let child_root = match &resolved { BundleNode::Db(child) => protocol_root(child.root), BundleNode::Official(child) => protocol_root(child.root), }; let child_key = ProtocolCacheKey::new( polymodel_renderer_protocol::NormalizedPath::new(resolved_path.as_str()) .map_err(|_| invalid_request("invalid companion path"))?, child_root, &child_bytes, ); target_keys_by_path.insert((entry.root, entry.path.clone()), child_key.clone()); target_keys_by_path.insert( (child_root_local(&resolved), resolved_path.clone()), child_key.clone(), ); children.push((resolved_path, resolved, child_bytes, child_key)); } }
for dependency in auxiliary_dependencies { let Some(directory) = libdir else { break; }; let Some(official) = resolve_official_asset(dependency.path.as_str(), directory).await else { if dependency.required { return Err(not_found()); } continue; }; let path = official.path.clone(); if target_keys_by_path.contains_key(&(official.root, path.clone())) { continue; } let resource_root = official.root; let key = ProtocolCacheKey::new( polymodel_renderer_protocol::NormalizedPath::new(path.as_str()) .map_err(|_| invalid_request("invalid official auxiliary path"))?, protocol_root(resource_root), &official.bytes, ); let aux_path = polymodel_renderer_protocol::NormalizedPath::new(path.as_str()) .map_err(|_| invalid_request("invalid official resource path"))?; let aux_protocol_root = protocol_root(resource_root); if !resolved_identities.insert((aux_protocol_root, aux_path.clone())) { continue; } target_keys_by_path.insert((resource_root, path.clone()), key.clone()); resolved_closure .resolved .push(polymodel_ldraw_core::ResolvedResource { path: aux_path, root: aux_protocol_root, sha256: official.sha256.clone(), byte_length: u64::try_from(official.bytes.len()) .map_err(|_| invalid_request("official resource byte length overflow"))?, bytes: official.bytes.clone(), mime_type: match official.mime_type.as_str() { "image/png" => "image/png", "image/jpeg" => "image/jpeg", "image/webp" => "image/webp", _ => "application/octet-stream", }, required: dependency.required, fallback_available: dependency.fallback_available, }); resources.push((BundleNode::Official(official.clone()), official.bytes)); } let node_root = child_root_local(&node); let node_path = match &node { BundleNode::Db(resolved) => resolved.path.clone(), BundleNode::Official(official) => official.path.clone(), }; let node_mime = match &node { BundleNode::Db(resolved) if resolved.mime_type == "image/png" => "image/png", BundleNode::Db(resolved) if resolved.mime_type == "image/jpeg" => "image/jpeg", BundleNode::Db(resolved) if resolved.mime_type == "image/webp" => "image/webp", BundleNode::Db(_) => "application/octet-stream", BundleNode::Official(official) if official.mime_type == "image/png" => "image/png", BundleNode::Official(official) if official.mime_type == "image/jpeg" => "image/jpeg", BundleNode::Official(official) if official.mime_type == "image/webp" => "image/webp", BundleNode::Official(_) => "application/octet-stream", }; let node_sha256 = match &node { BundleNode::Db(resolved) => binding_for(&project, resolved)?.sha256, BundleNode::Official(official) => official.sha256.clone(), }; let node_path_normalized = polymodel_renderer_protocol::NormalizedPath::new(node_path.as_str()) .map_err(|_| invalid_request("invalid resolved resource path"))?; let node_protocol_root = protocol_root(node_root); if !resolved_identities.insert((node_protocol_root, node_path_normalized.clone())) { for (child_path, child_node, child_bytes, child_key) in children { queue.push_back((child_path, child_node, child_bytes, child_key)); } continue; } resolved_closure .resolved .push(polymodel_ldraw_core::ResolvedResource { path: node_path_normalized, root: node_protocol_root, sha256: node_sha256, byte_length: u64::try_from(bytes.len()) .map_err(|_| invalid_request("resolved resource byte length overflow"))?, bytes: bytes.clone(), mime_type: node_mime, required: false, fallback_available: true, }); resources.push((node, bytes)); for (child_path, child_node, child_bytes, child_key) in children { queue.push_back((child_path, child_node, child_bytes, child_key)); } }
resolved_closure .validate() .map_err(|error| invalid_request(format!("invalid resolved resource closure: {error}")))?; encode_bundle_response( &project, &path, &resources, source_identities, target_selections, ) .await}
async fn encode_bundle_response( project: &AtUri, path: &CanonicalPath, resources: &[(BundleNode, Vec<u8>)], source_identities: Vec<ProtocolCacheKey>, target_selections: Vec<TargetSelection>,) -> AppResult<Response> { let mut bundled = Vec::with_capacity(resources.len()); let mut metadata_bytes = path.as_str().len(); for (node, bytes) in resources { let bundled_resource = match node { BundleNode::Db(resolved) => { let binding = binding_for(project, resolved)?; ModelResource { key: resolved.path.to_string(), path: resolved.path.to_string(), root: resolved.root.as_str().to_owned(), byte_length: binding.byte_length, sha256: binding.sha256.as_slice().try_into().map_err(|_| { invalid_request("stored resource digest is not SHA-256") })?, mime_type: resolved.mime_type.clone(), target_cids: binding.target_cids.into_boxed_slice(), bytes: bytes.clone().into_boxed_slice(), } } BundleNode::Official(official) => ModelResource { key: official.path.to_string(), path: official.path.to_string(), root: official.root.as_str().to_owned(), byte_length: u64::try_from(official.bytes.len()) .map_err(|_| invalid_request("official resource byte length overflow"))?, sha256: official .sha256 .clone() .try_into() .map_err(|_| invalid_request("official resource digest is not SHA-256"))?, mime_type: official.mime_type.clone(), target_cids: Box::default(), bytes: bytes.clone().into_boxed_slice(), }, }; bundled_resource .validate() .map_err(|error| invalid_request(format!("invalid model resource: {error:?}")))?; let resource_metadata = bundled_resource .key .len() .checked_add(bundled_resource.path.len()) .and_then(|size| size.checked_add(bundled_resource.root.len())) .and_then(|size| size.checked_add(bundled_resource.mime_type.len())) .and_then(|size| { bundled_resource .target_cids .iter() .try_fold(size, |total, cid| total.checked_add(cid.len())) }) .ok_or_else(|| invalid_request("model bundle metadata bound overflow"))?; metadata_bytes = metadata_bytes .checked_add(resource_metadata) .ok_or_else(|| invalid_request("model bundle metadata bound overflow"))?; bundled.push(bundled_resource); } if metadata_bytes > MAX_MODEL_METADATA_BYTES { return Err(invalid_request("model bundle metadata bound exceeded")); } let bundle = ModelLoadBundle { version: PROTOCOL_VERSION, format: MeshFormat::CompoundLdraw, primary: path.to_string(), resources: bundled.into_boxed_slice(), source_identities, target_selections, }; let encoded = bundle .encode() .map_err(|error| invalid_request(format!("model bundle encoding failed: {error:?}")))?; Response::builder() .status(StatusCode::OK) .header( header::CONTENT_TYPE, "application/vnd.polymodel.load-bundle", ) .header(header::CONTENT_LENGTH, encoded.len()) .body(Body::from(encoded)) .map_err(|error| internal(format!("model bundle response build failed: {error}")))}
pub(super) async fn fetch_resource( State(state): State<AppState>, AxumPath(binding): AxumPath<String>,) -> AppResult<Response> { let binding = decode_binding(&binding).map_err(|_| not_found())?; let resolver = SqliteResolver::new(state.pool.clone()); let resource = resolver .resolve(&binding.project, &binding.path) .await .map_err(|_| not_found())?; if binding_for(&binding.project, &resource).map_err(|_| not_found())? != binding { return Err(not_found()); } let bytes = fetch_verified_bytes(&state, &resource, binding.byte_length).await?; Response::builder() .status(StatusCode::OK) .header(header::CONTENT_TYPE, resource.mime_type.as_str()) .header(header::CONTENT_LENGTH, bytes.len()) .body(Body::from_stream(stream::once(async move { Ok::<_, std::convert::Infallible>(bytes::Bytes::from(bytes)) }))) .map_err(|error| internal(format!("LDraw response build failed: {error}")))}
pub(super) fn binding_for(project: &AtUri, resource: &ResolvedResource) -> AppResult<RouteBinding> { let publisher = resource.source.resource_uri.authority().convert(); let publisher = match publisher { jacquard_common::types::string::AtIdentifier::Did(did) => did, jacquard_common::types::string::AtIdentifier::Handle(_) => { return Err(internal("resolved LDraw source has a handle authority")); } }; let byte_length = u64::try_from(resource.byte_length) .map_err(|_| invalid_request("negative LDraw resource length"))?; if resource.sha256.len() != 32 || resource.target.blob_cids.is_empty() { return Err(not_found()); } Ok(RouteBinding { version: ROUTE_VERSION, project: project.clone(), publisher, source: resource.source.resource_uri.clone(), root: resource.root, path: resource.path.clone(), target_cids: resource.target.blob_cids.clone(), byte_length, sha256: resource.sha256.clone(), })}
#[cfg(test)]pub(super) fn encode_binding(binding: &RouteBinding) -> AppResult<String> { let payload = RoutePayload { v: binding.version, project: &binding.project, publisher: &binding.publisher, source: &binding.source, root: binding.root.as_str(), path: binding.path.as_str(), cids: &binding.target_cids, len: binding.byte_length, sha256: &binding.sha256, }; let bytes = serde_json::to_vec(&payload) .map_err(|error| internal(format!("route binding serialization failed: {error}")))?; let digest = Sha256::digest(&bytes); let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes); Ok(format!("{ROUTE_PREFIX}{encoded}.{}", hex_bytes(&digest)))}
fn decode_binding(value: &str) -> Result<RouteBinding, ()> { let value = value.strip_prefix(ROUTE_PREFIX).unwrap_or(value); let (payload, digest) = value.rsplit_once('.').ok_or(())?; let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD .decode(payload) .map_err(|_| ())?; if digest != hex_bytes(&Sha256::digest(&bytes)) { return Err(()); } let payload: RoutePayloadOwned = serde_json::from_slice(&bytes).map_err(|_| ())?; let root = parse_root(&payload.root).ok_or(())?; let path = CanonicalPath::parse(&payload.path).map_err(|_| ())?; if payload.v != ROUTE_VERSION || payload.sha256.len() != 32 || payload.cids.is_empty() { return Err(()); } Ok(RouteBinding { version: payload.v, project: payload.project, publisher: payload.publisher, source: payload.source, root, path, target_cids: payload.cids, byte_length: payload.len, sha256: payload.sha256, })}
fn protocol_root(root: RootId) -> ProtocolRootId { match root { RootId::Mpd => ProtocolRootId::CurrentMpd, RootId::Manifest => ProtocolRootId::UploadedManifest, RootId::Models | RootId::Parts | RootId::P | RootId::External => { ProtocolRootId::UploadedLdraw } RootId::OfficialParts | RootId::OfficialP => ProtocolRootId::OfficialLibrary, }}
fn parse_root(value: &str) -> Option<RootId> { Some(match value { "mpd" => RootId::Mpd, "manifest" => RootId::Manifest, "models" => RootId::Models, "parts" => RootId::Parts, "p" => RootId::P, "external" => RootId::External, "official-parts" => RootId::OfficialParts, "official-p" => RootId::OfficialP, _ => return None, })}
fn validate_manifest_child( parent: &ResolvedResource, child: &ResolvedResource, manifest: &[ManifestEntry],) -> AppResult<()> { let entry = manifest .iter() .find(|entry| entry.root == child.root && entry.path == child.path) .ok_or_else(not_found)?; let child_cid = child.target.blob_cids.first().map(String::as_str); if entry.target_resource_uri.is_empty() || entry.target_cid.is_empty() || entry.byte_length < 0 || entry.sha256.len() != 32 || entry.mime_type.is_empty() || child_cid.is_none() || !contains_in_scope( parent.root, parent.path.as_str(), child.root, child.path.as_str(), ) || child.resource.uri.as_ref() != entry.target_resource_uri || child_cid != Some(entry.target_cid.as_str()) || child.byte_length != entry.byte_length || child.sha256 != entry.sha256 || child.mime_type != entry.mime_type { return Err(not_found()); } Ok(())}
fn reachable_mpd_sections( current: &CanonicalPath, references: &[polymodel_ldraw_core::MpdReference],) -> HashSet<usize> { if !references.iter().any(|reference| reference.virtual_file) { return references .iter() .map(|reference| reference.file_ordinal) .collect(); } if let Some(section) = current.as_str().strip_prefix("mpd/") { return references .iter() .filter(|reference| reference.file_name.eq_ignore_ascii_case(section)) .map(|reference| reference.file_ordinal) .collect(); }
let mut sections_by_name = HashMap::new(); for reference in references { sections_by_name .entry(reference.file_name.to_ascii_lowercase()) .or_insert(reference.file_ordinal); if let Some((_, suffix)) = reference.file_name.rsplit_once(" - ") { sections_by_name .entry(suffix.to_ascii_lowercase()) .or_insert(reference.file_ordinal); } }
let mut reachable = HashSet::from([0_usize]); let mut worklist = VecDeque::from([0_usize]); while let Some(file_ordinal) = worklist.pop_front() { for reference in references.iter().filter(|reference| { reference.file_ordinal == file_ordinal && reference.target_virtual_file }) { let target = reference.target.to_ascii_lowercase(); let Some(&target_ordinal) = sections_by_name.get(&target) else { continue; }; if reachable.insert(target_ordinal) { worklist.push_back(target_ordinal); } } } reachable}
fn core_reference_path( current: &CanonicalPath, reference: &polymodel_ldraw_core::MpdReference,) -> Option<CanonicalPath> { let current = polymodel_ldraw_core::NormalizedPath::new(current.as_str()).ok()?; let resolved = polymodel_ldraw_core::resolve_reference_path(¤t, reference).ok()?; CanonicalPath::parse(resolved.path.as_str()).ok()}
/// Enforce the LDraw root and subroot policy after PM-76 has selected a candidate.////// Manifest references may cross ordinary LDraw roots (for example a model may/// reference a part), but they may not escape an explicit `s/`, `8/`, or `48/`/// subroot. There is deliberately no parent-directory fallback: a missing/// exact candidate is a resolver miss, not permission to reinterpret its name.fn contains_in_scope(parent_root: RootId, parent: &str, child_root: RootId, child: &str) -> bool { let Ok(parent_scope) = subroot_scope(parent_root, parent) else { return false; }; let Ok(child_scope) = subroot_scope(child_root, child) else { return false; }; match (parent_scope, child_scope) { (Some(parent), Some(child)) => parent_root == child_root && parent == child, (Some(_), None) | (None, Some(_)) => false, (None, None) => true, }}
fn subroot_scope(root: RootId, path: &str) -> Result<Option<&str>, ()> { let segments = path.split('/').collect::<Vec<_>>(); let first = segments.first().copied().ok_or(())?; let expected = root.as_str(); if first != expected && !(root.case_insensitive() && first.eq_ignore_ascii_case(expected)) { return Err(()); } if root == RootId::External { return if segments.len() == 3 { Ok(None) } else { Err(()) }; } if segments.len() <= 2 { return Ok(None); } if !matches!( root, RootId::Parts | RootId::P | RootId::OfficialParts | RootId::OfficialP ) { return Err(()); } match segments.get(1).copied() { Some(prefix) if prefix.eq_ignore_ascii_case("s") => Ok(Some("s")), Some(prefix) if prefix.eq_ignore_ascii_case("8") => Ok(Some("8")), Some(prefix) if prefix.eq_ignore_ascii_case("48") => Ok(Some("48")), _ => Err(()), }}
async fn fetch_verified_bytes( state: &AppState, resource: &ResolvedResource, max_bytes: u64,) -> AppResult<Vec<u8>> { let did = match resource.source.resource_uri.authority().convert() { jacquard_common::types::string::AtIdentifier::Did(did) => did, jacquard_common::types::string::AtIdentifier::Handle(handle) => { super::actor::resolve_actor(state, &handle.convert().into()).await? } }; let did_doc_response = state .resolver .resolve_did_doc(&did) .await .map_err(|error| internal(format!("DID document resolution failed: {error}")))?; let did_doc = did_doc_response .parse() .map_err(|error| internal(format!("DID document parse failed: {error}")))?; let endpoint = did_doc .pds_endpoint() .ok_or_else(|| internal("DID document does not advertise a PDS endpoint"))? .to_owned(); let expected_length = u64::try_from(resource.byte_length) .map_err(|_| invalid_request("negative LDraw resource length"))?; if expected_length != max_bytes || expected_length > MAX_MODEL_RESOURCE_BYTES { return Err(invalid_request("LDraw byte admission failed")); } let capacity = usize::try_from(expected_length) .map_err(|_| invalid_request("LDraw byte length is too large"))?; let mut bytes = Vec::with_capacity(capacity); let mut admitted = 0_u64; for value in &resource.source.blob_cids { let cid = Cid::new_owned(value.as_bytes()) .map_err(|error| internal(format!("invalid LDraw blob CID: {error}")))?; let response = state .resolver .xrpc(endpoint.borrow()) .download(&GetBlob { did: did.clone(), cid, }) .await .map_err(|error| internal(format!("blob fetch failed: {error}")))?; if !response.status().is_success() { return Err(internal(format!( "blob fetch failed with HTTP {}", response.status() ))); } let (_, body) = response.into_parts(); let mut body_stream = body.into_inner(); let mut chunk = Vec::new(); while let Some(part) = body_stream .try_next() .await .map_err(|error| internal(format!("blob stream failed: {error}")))? { let part_length = u64::try_from(part.len()) .map_err(|_| invalid_request("LDraw chunk length overflow"))?; admitted = admitted .checked_add(part_length) .ok_or_else(|| invalid_request("LDraw byte length overflow"))?; if admitted > max_bytes { return Err(invalid_request("LDraw byte admission exceeded")); } chunk.extend_from_slice(&part); } verify_chunk_cid(value, &chunk) .map_err(|error| invalid_request(format!("chunk integrity check failed: {error}")))?; bytes.extend_from_slice(&chunk); } if admitted != expected_length || bytes.len() as u64 != expected_length { return Err(invalid_request("LDraw byte length verification failed")); } let digest = Sha256::digest(&bytes); if &digest[..] != resource.sha256.as_slice() { return Err(invalid_request("LDraw SHA-256 verification failed")); } Ok(bytes)}
pub(super) async fn fetch_staged_bytes( state: &AppState, did: &Did, blob_cids: &[String], expected_length: u64, expected_sha256: &[u8], max_bytes: u64,) -> AppResult<Vec<u8>> { if expected_length == 0 || expected_length > max_bytes || expected_length > MAX_MODEL_RESOURCE_BYTES || expected_sha256.len() != 32 { return Err(invalid_request("staged preview byte admission failed")); } let did_doc_response = state .resolver .resolve_did_doc(did) .await .map_err(|error| internal(format!("DID document resolution failed: {error}")))?; let did_doc = did_doc_response .parse() .map_err(|error| internal(format!("DID document parse failed: {error}")))?; let endpoint = did_doc .pds_endpoint() .ok_or_else(|| internal("DID document does not advertise a PDS endpoint"))? .to_owned(); let mut bytes = Vec::with_capacity( usize::try_from(expected_length) .map_err(|_| invalid_request("staged preview byte length is too large"))?, ); for value in blob_cids { let cid = Cid::new_owned(value.as_bytes()) .map_err(|error| internal(format!("invalid staged blob CID: {error}")))?; let response = state .resolver .xrpc(endpoint.borrow()) .download(&GetBlob { did: did.clone(), cid, }) .await .map_err(|error| internal(format!("blob fetch failed: {error}")))?; if !response.status().is_success() { return Err(internal(format!( "blob fetch failed with HTTP {}", response.status() ))); } let mut stream = response.into_parts().1.into_inner(); let mut chunk = Vec::new(); while let Some(part) = stream .try_next() .await .map_err(|error| internal(format!("blob stream failed: {error}")))? { if bytes .len() .saturating_add(chunk.len()) .saturating_add(part.len()) > expected_length as usize { return Err(invalid_request("staged preview byte admission exceeded")); } chunk.extend_from_slice(&part); } verify_chunk_cid(value, &chunk) .map_err(|error| invalid_request(format!("chunk integrity check failed: {error}")))?; bytes.extend_from_slice(&chunk); } if bytes.len() as u64 != expected_length || &Sha256::digest(&bytes)[..] != expected_sha256 { return Err(invalid_request( "staged preview integrity verification failed", )); } Ok(bytes)}
fn hex_bytes(bytes: &[u8]) -> String { bytes.iter().map(|byte| format!("{byte:02x}")).collect()}
fn resolve_error(error: crate::ldraw::ResolveError) -> super::error::AppError { match error { crate::ldraw::ResolveError::Unavailable { .. } => internal("LDraw resolver unavailable"), _ => not_found(), }}
#[cfg(test)]mod tests { use super::*;
#[test] fn route_binding_is_deterministic_and_rejects_substitution() { let project = AtUri::new_owned("at://did:plc:project/space.polymodel.library.thing/main").unwrap(); let publisher = Did::new_owned("did:plc:publisher").unwrap(); let source = AtUri::new_owned("at://did:plc:publisher/space.polymodel.library.part/a").unwrap(); let binding = RouteBinding { version: ROUTE_VERSION, project, publisher, source, root: RootId::Models, path: CanonicalPath::parse("models/main.ldr").unwrap(), target_cids: vec!["bafkreigh2akiscaildc3".into()], byte_length: 4, sha256: vec![0; 32], }; let first = encode_binding(&binding).unwrap(); assert_eq!(first, encode_binding(&binding).unwrap()); assert_eq!(decode_binding(&first).unwrap(), binding); let mut changed = first.into_bytes(); let index = changed.len() - 1; changed[index] = if changed[index] == b'0' { b'1' } else { b'0' }; assert!(decode_binding(std::str::from_utf8(&changed).unwrap()).is_err()); }
#[test] fn reference_joins_cover_mpd_roots_subroots_and_relative_paths() { let current = CanonicalPath::parse("models/sub/main.ldr").unwrap(); let span = polymodel_ldraw_core::Span { start: 0, end: 0, line: 0, column: 0, }; let relative = polymodel_ldraw_core::MpdReference { file_name: "main.ldr".into(), file_ordinal: 0, target: "child.dat".into(), span, virtual_file: false, target_virtual_file: false, }; assert_eq!( core_reference_path(¤t, &relative).unwrap().as_str(), "p/child.dat" ); for target in ["models/other.ldr", "parts/3001.dat", "p/foo.dat"] { let explicit = polymodel_ldraw_core::MpdReference { target: target.into(), ..relative.clone() }; assert_eq!( core_reference_path(¤t, &explicit).unwrap().as_str(), target ); } for (prefix, root) in [("s", "parts"), ("S", "parts"), ("8", "p"), ("48", "p")] { let explicit = polymodel_ldraw_core::MpdReference { target: format!("{prefix}/3001.dat"), ..relative.clone() }; // The s/ alias maps subparts to the parts/ root without preserving // the s/ segment in the canonical path (subparts are stored as // parts/{name} in the official library). The 8/ and 48/ aliases // preserve their segment under the p/ root. let resolved = core_reference_path(¤t, &explicit).unwrap(); let expected = match prefix { "s" | "S" => format!("{root}/3001.dat"), _ => format!("{root}/{prefix}/3001.dat"), }; assert_eq!(resolved.as_str(), expected); } let mpd = polymodel_ldraw_core::MpdReference { file_name: "main.ldr".into(), file_ordinal: 0, target: "Child.DAT".into(), span, virtual_file: true, target_virtual_file: true, }; assert_eq!( core_reference_path(¤t, &mpd).unwrap().as_str(), "mpd/child.dat" ); }
#[test] fn mismatched_uploaded_mpd_name_keeps_first_declared_root_section() { let bytes = b"0 FILE 10030 - main.ldr\n1 16 0 0 0 1 0 0 0 1 0 0 0 0 3001.dat\n0 FILE child.ldr\n1 16 0 0 0 1 0 0 0 1 0 0 0 0 3002.dat\n0 NOFILE\n"; let references = polymodel_ldraw_core::extract_references(bytes).unwrap(); let uploaded = CanonicalPath::parse("models/omr-10030-1.mpd").unwrap(); let reachable = reachable_mpd_sections(&uploaded, &references); let eligible = references .iter() .filter(|reference| { !reference.target_virtual_file && reachable.contains(&reference.file_ordinal) }) .map(|reference| reference.target.as_str()) .collect::<Vec<_>>(); assert_eq!(eligible, ["3001.dat"]); }
#[test] fn real_mismatched_mpd_root_exposes_ordinary_references() { let references = polymodel_ldraw_core::extract_references(include_bytes!( "../../crates/polymodel-ldraw-testkit/corpus/omr-10030-1.mpd" )) .unwrap(); let uploaded = CanonicalPath::parse("models/omr-10030-1.mpd").unwrap(); let reachable = reachable_mpd_sections(&uploaded, &references); let root_references = references.iter().filter(|reference| { !reference.target_virtual_file && reachable.contains(&reference.file_ordinal) }); assert!( root_references .clone() .any(|reference| reference.target == "2625.dat") ); assert!(root_references.count() > 10); }
#[test] fn reference_joins_fail_closed_and_mpd_sections_are_isolated() { let current = CanonicalPath::parse("mpd/sub/root.ldr").unwrap(); let span = polymodel_ldraw_core::Span { start: 0, end: 0, line: 0, column: 0, }; let malformed = polymodel_ldraw_core::MpdReference { file_name: "sub/root.ldr".into(), file_ordinal: 0, target: "../escape.dat".into(), span, virtual_file: false, target_virtual_file: false, }; assert!(core_reference_path(¤t, &malformed).is_none()); let first = polymodel_ldraw_core::MpdReference { file_name: "sub/root.ldr".into(), file_ordinal: 0, target: "child.dat".into(), span, virtual_file: true, target_virtual_file: false, }; let second = polymodel_ldraw_core::MpdReference { file_name: "child.dat".into(), file_ordinal: 1, target: "root.ldr".into(), span, virtual_file: true, target_virtual_file: true, }; let references = [first.clone(), second.clone()]; let reachable = reachable_mpd_sections(¤t, &references); assert!(reachable.contains(&first.file_ordinal)); assert!(!reachable.contains(&second.file_ordinal)); let mut children = Vec::new(); let mut seen = HashSet::new(); let child = CanonicalPath::parse("parts/3001.dat").unwrap(); if seen.insert(child.clone()) { children.push(child.clone()); } if seen.insert(child.clone()) { children.push(child); } assert_eq!(children.len(), 1); }
#[test] fn scope_requires_same_root_and_containment() { assert!(contains_in_scope( RootId::Models, "models/main.ldr", RootId::Models, "models/main.ldr" )); assert!(contains_in_scope( RootId::Models, "models/main.ldr", RootId::Parts, "parts/3001.dat" )); assert!(!contains_in_scope( RootId::Models, "models/main.ldr", RootId::Models, "official-parts/x.dat" )); assert!(contains_in_scope( RootId::Models, "models/main.ldr", RootId::Models, "models/other.ldr" )); }
#[test] fn scope_preserves_explicit_standard_subroots() { for subroot in ["s", "8", "48"] { let parent = format!("parts/{subroot}/parent.dat"); let child = format!("parts/{subroot}/child.dat"); assert!(contains_in_scope( RootId::Parts, &parent, RootId::Parts, &child )); assert!(!contains_in_scope( RootId::Parts, &parent, RootId::Parts, "parts/other/child.dat" )); assert!(!contains_in_scope( RootId::Parts, &parent, RootId::Models, &child )); } assert!(contains_in_scope( RootId::P, "p/S/parent.dat", RootId::P, "P/S/child.dat" )); assert!(contains_in_scope( RootId::Parts, "parts/s/parent.dat", RootId::Parts, "parts/S/child.dat" )); }
#[test] fn scope_rejects_unknown_or_malformed_root_prefixes() { assert!(contains_in_scope( RootId::External, "external/xdid_3aplc/xpart", RootId::External, "external/xdid_3aplc/xother" )); assert!(!contains_in_scope( RootId::External, "external/xdid_3aplc/xpart", RootId::External, "External/xdid_3aplc/xother" )); assert!(!contains_in_scope( RootId::External, "external/xdid_3aplc/xpart", RootId::External, "external/xdid_3aplc/nested/xother" )); assert!(!contains_in_scope( RootId::Parts, "parts/s/parent.dat", RootId::Parts, "models/s/child.dat" )); assert!(!contains_in_scope( RootId::Parts, "parts/s/parent.dat", RootId::Parts, "parts/unknown/child.dat" )); assert!(!contains_in_scope( RootId::Parts, "wrong/s/parent.dat", RootId::Parts, "parts/s/child.dat" )); }
// --- Official LDraw library filesystem fallback tests ---
use super::super::test_support::{self as test_support, seed_compound_resource}; use polymodel_renderer_protocol::ModelLoadBundle; use sha2::Digest; use std::collections::HashMap;
const DID_A: &str = test_support::DID_A;
/// Build minimal valid LDraw bytes for a part that references a primitive. fn ldraw_part_bytes() -> Vec<u8> { // A part referencing p/4-4disc.dat via type-1 line. b"0 FILE foo.dat\n1 16 0 0 0 1 0 0 0 1 0 0 0 1 4-4disc.dat\n".to_vec() }
/// Build minimal valid LDraw bytes for a primitive (no references). fn ldraw_primitive_bytes() -> Vec<u8> { b"0 Unofficial primitive\n3 16 0 0 0 1 0 0 0 1 0\n".to_vec() }
#[tokio::test] async fn official_library_fallback_resolves_parts_and_primitives() { let project = format!("at://{DID_A}/space.polymodel.library.thing/mpd"); let root_uri = format!("at://{DID_A}/space.polymodel.library.part/root");
// Root MPD: references parts/foo.dat twice. let root_bytes = b"0 FILE main.ldr\n1 16 0 0 0 1 0 0 0 1 0 0 0 1 parts/foo.dat\n1 16 0 0 0 1 0 0 0 1 0 0 0 1 parts/foo.dat\n".to_vec(); let (root_cid, _) = test_support::test_blob(&root_bytes);
// Serve root_bytes from the loopback PDS. let (pds, _server) = test_support::loopback_pds_with_blobs(HashMap::from([( root_cid.clone(), root_bytes.clone(), )])) .await;
// Create tempdir fake official library. let tmp = tempfile::tempdir().unwrap(); let libdir = tmp.path(); tokio::fs::create_dir_all(libdir.join("parts")) .await .unwrap(); tokio::fs::create_dir_all(libdir.join("p")).await.unwrap(); let part_bytes = ldraw_part_bytes(); let prim_bytes = ldraw_primitive_bytes(); tokio::fs::write(libdir.join("parts").join("foo.dat"), &part_bytes) .await .unwrap(); tokio::fs::write(libdir.join("p").join("4-4disc.dat"), &prim_bytes) .await .unwrap();
// Set up DB with root resource. let pool = test_support::pool().await; seed_compound_resource( &pool, &project, &root_uri, "models", "models/main.ldr", &root_bytes, &root_cid, ) .await;
let bootstrap = crate::oauth::bootstrap_oauth(pool.clone(), Some("http://localhost")) .expect("ephemeral OAuth bootstrap"); let state = AppState::new_with_resolver_and_library( pool, bootstrap, test_support::loopback_resolver(&pds), Some(libdir.to_owned()), ); let app = crate::appview::router().with_state(state);
let request = axum::http::Request::builder() .method("POST") .uri("/app/model-load-bundle") .header("content-type", "application/json") .body(axum::body::Body::from( serde_json::json!({"uri": root_uri}).to_string(), )) .unwrap(); let response = tower::ServiceExt::oneshot(app, request).await.unwrap(); assert_eq!(response.status(), axum::http::StatusCode::OK); let body = http_body_util::BodyExt::collect(response.into_body()) .await .unwrap() .to_bytes(); let bundle = ModelLoadBundle::decode(&body).unwrap(); assert!(bundle.validate().is_ok()); let keys: Vec<&str> = bundle.resources.iter().map(|r| r.key.as_str()).collect(); assert!(keys.contains(&"models/main.ldr"), "missing root: {keys:?}"); assert!(keys.contains(&"parts/foo.dat"), "missing part: {keys:?}"); assert!( keys.contains(&"p/4-4disc.dat"), "missing primitive: {keys:?}" ); assert_eq!( bundle.resources.len(), 3, "duplicate references must not duplicate resources" ); assert_eq!( bundle.target_selections.len(), 3, "duplicate references must retain both edges" );
// Digests match. let part_resource = bundle .resources .iter() .find(|r| r.key == "parts/foo.dat") .unwrap(); assert_eq!(&part_resource.sha256[..], &Sha256::digest(&part_bytes)[..]); assert_eq!(part_resource.bytes.as_ref(), part_bytes.as_slice()); assert_eq!(part_resource.root, "official-parts"); assert_eq!(part_resource.mime_type, "application/x-ldraw"); assert!(part_resource.target_cids.is_empty());
let prim_resource = bundle .resources .iter() .find(|r| r.key == "p/4-4disc.dat") .unwrap(); assert_eq!(&prim_resource.sha256[..], &Sha256::digest(&prim_bytes)[..]); assert_eq!(prim_resource.bytes.as_ref(), prim_bytes.as_slice()); assert_eq!(prim_resource.root, "official-p"); assert!(prim_resource.target_cids.is_empty()); }
#[tokio::test] async fn official_library_missing_part_returns_404() { let project = format!("at://{DID_A}/space.polymodel.library.thing/mpd"); let root_uri = format!("at://{DID_A}/space.polymodel.library.part/root"); let root_bytes = b"0 FILE main.ldr\n1 16 0 0 0 1 0 0 0 1 0 0 0 1 parts/nonexistent.dat\n".to_vec(); let (root_cid, _) = test_support::test_blob(&root_bytes); let (pds, _server) = test_support::loopback_pds_with_blobs(HashMap::from([( root_cid.clone(), root_bytes.clone(), )])) .await;
// Empty official library (no files). let tmp = tempfile::tempdir().unwrap(); let libdir = tmp.path(); tokio::fs::create_dir_all(libdir.join("parts")) .await .unwrap();
let pool = test_support::pool().await; seed_compound_resource( &pool, &project, &root_uri, "models", "models/main.ldr", &root_bytes, &root_cid, ) .await; let bootstrap = crate::oauth::bootstrap_oauth(pool.clone(), Some("http://localhost")) .expect("ephemeral OAuth bootstrap"); let state = AppState::new_with_resolver_and_library( pool, bootstrap, test_support::loopback_resolver(&pds), Some(libdir.to_owned()), ); let app = crate::appview::router().with_state(state);
let request = axum::http::Request::builder() .method("POST") .uri("/app/model-load-bundle") .header("content-type", "application/json") .body(axum::body::Body::from( serde_json::json!({"uri": root_uri}).to_string(), )) .unwrap(); let response = tower::ServiceExt::oneshot(app, request).await.unwrap(); assert_eq!(response.status(), axum::http::StatusCode::NOT_FOUND); }
#[test] fn canonical_path_rejects_traversal_attempts() { // CanonicalPath parsing already enforces `..` and `%` rejection. assert!(CanonicalPath::parse("parts/../etc/passwd").is_err()); assert!(CanonicalPath::parse("parts/%2e%2e/escape.dat").is_err()); assert!(CanonicalPath::parse("p/../../escape.dat").is_err()); assert!(CanonicalPath::parse("parts/foo.dat").is_ok()); }
#[tokio::test] async fn no_library_dir_behavior_unchanged() { // With ldraw_library_dir = None, the official fallback does not activate. // A reference to parts/foo.dat with no DB row and no library dir → 404. let project = format!("at://{DID_A}/space.polymodel.library.thing/mpd"); let root_uri = format!("at://{DID_A}/space.polymodel.library.part/root"); let root_bytes = b"0 FILE main.ldr\n1 16 0 0 0 1 0 0 0 1 0 0 0 1 parts/foo.dat\n".to_vec(); let (root_cid, _) = test_support::test_blob(&root_bytes); let (pds, _server) = test_support::loopback_pds_with_blobs(HashMap::from([( root_cid.clone(), root_bytes.clone(), )])) .await;
let pool = test_support::pool().await; seed_compound_resource( &pool, &project, &root_uri, "models", "models/main.ldr", &root_bytes, &root_cid, ) .await; let bootstrap = crate::oauth::bootstrap_oauth(pool.clone(), Some("http://localhost")) .expect("ephemeral OAuth bootstrap"); // No library dir: None let state = AppState::new_with_resolver(pool, bootstrap, test_support::loopback_resolver(&pds)); let app = crate::appview::router().with_state(state);
let request = axum::http::Request::builder() .method("POST") .uri("/app/model-load-bundle") .header("content-type", "application/json") .body(axum::body::Body::from( serde_json::json!({"uri": root_uri}).to_string(), )) .unwrap(); let response = tower::ServiceExt::oneshot(app, request).await.unwrap(); assert_eq!(response.status(), axum::http::StatusCode::NOT_FOUND); }}