diff --git a/src/pcp/index.rs b/src/pcp/index.rs index 1f3a148..4de5e49 100644 --- a/src/pcp/index.rs +++ b/src/pcp/index.rs @@ -7,9 +7,11 @@ use std::borrow::Cow; use std::collections::hash_map::Entry; use std::collections::{HashMap, HashSet}; +use std::path::PathBuf; use anyhow::Result; +use crate::ar::Resolver; use crate::sdf::schema::{ChildrenKey, FieldKey}; use crate::sdf::{LayerData, LayerOffset, ListOp, Path, Payload, PayloadListOp, Reference, Specifier, Value}; @@ -903,7 +905,12 @@ impl<'a> IndexBuilder<'a> { fn get_sublayer_stack(&self, root_layer: usize) -> Vec<(usize, LayerOffset)> { self.stack.sublayer_stacks.get(&root_layer).cloned().unwrap_or_else(|| { - LayerStack::build_sublayer_stack(root_layer, &self.stack.layers, &self.stack.identifiers) + LayerStack::build_sublayer_stack( + root_layer, + &self.stack.layers, + &self.stack.identifiers, + &*self.stack.resolver, + ) }) } @@ -1085,7 +1092,7 @@ impl<'a> IndexBuilder<'a> { // External reference — evaluate in a fresh sub-builder so the // target's layer stack doesn't share our `seen` set. The sub-builder // uses its own ancestor context derived from the target path. - let Some(layer_index) = find_layer(asset_path, &self.stack.identifiers) else { + let Some(layer_index) = find_layer(asset_path, &self.stack.identifiers, &*self.stack.resolver) else { return Err(Error::UnresolvedLayer { asset_path: asset_path.to_string(), arc, @@ -1323,12 +1330,19 @@ fn collect_payloads_in(nodes: &[Node], layers: &[LayerData]) -> Vec { /// Finds a layer index whose identifier matches `asset_path`. /// -/// Tries an exact match first, then falls back to suffix matching at a -/// path separator boundary. Strips leading `./` before matching. -pub(super) fn find_layer(asset_path: &str, identifiers: &[String]) -> Option { +/// Tries an exact match first, then suffix-matches at a path-separator +/// boundary. For relative paths that traverse parent directories (`../foo`, +/// `..\foo`), these strategies fail against canonical absolute identifiers. +/// The resolver anchors the path against each candidate identifier so that +/// custom asset resolution backends work correctly without any filesystem +/// access in this function. +pub(super) fn find_layer(asset_path: &str, identifiers: &[String], resolver: &dyn Resolver) -> Option { + use crate::ar::ResolvedPath; + let sep = std::path::MAIN_SEPARATOR as u8; let needle = asset_path.strip_prefix("./").unwrap_or(asset_path); + // Fast path: exact or suffix match against canonical identifiers. for (i, id) in identifiers.iter().enumerate() { if *id == needle { return Some(i); @@ -1342,6 +1356,19 @@ pub(super) fn find_layer(asset_path: &str, identifiers: &[String]) -> Option anyhow::Result { let (layers, identifiers) = load_layers(path)?; - Ok(LayerStack::new(layers, identifiers, 0)) + Ok(LayerStack::new( + layers, + identifiers, + 0, + Box::new(DefaultResolver::new()), + )) } #[test] @@ -1511,29 +1543,62 @@ mod tests { #[test] fn find_layer_exact_match() -> Result<()> { + let resolver = DefaultResolver::new(); let (_, ids) = load_layers(&fixture_path("ref_external.usda"))?; - assert!(find_layer(&ids[0], &ids).is_some()); + assert!(find_layer(&ids[0], &ids, &resolver).is_some()); Ok(()) } #[test] fn find_layer_suffix_match() -> Result<()> { + let resolver = DefaultResolver::new(); let (_, ids) = load_layers(&fixture_path("ref_external.usda"))?; - assert!(find_layer("ref_target.usda", &ids).is_some()); + assert!(find_layer("ref_target.usda", &ids, &resolver).is_some()); Ok(()) } #[test] fn find_layer_no_partial_name_match() -> Result<()> { + let resolver = DefaultResolver::new(); let (_, ids) = load_layers(&fixture_path("ref_external.usda"))?; - assert!(find_layer("target.usda", &ids).is_none()); + assert!(find_layer("target.usda", &ids, &resolver).is_none()); Ok(()) } #[test] fn find_layer_not_found() -> Result<()> { + let resolver = DefaultResolver::new(); let (_, ids) = load_layers(&fixture_path("ref_external.usda"))?; - assert!(find_layer("nonexistent.usda", &ids).is_none()); + assert!(find_layer("nonexistent.usda", &ids, &resolver).is_none()); + Ok(()) + } + + /// Relative `../` paths must be anchored against each candidate + /// identifier's location via the resolver. Without this, a reference + /// like `../Materials/Materials.usd` authored inside a prop USD silently + /// fails every composition lookup with `UnresolvedLayer`. + #[test] + fn find_layer_relative_parent_anchored() -> Result<()> { + let tmp = tempfile::tempdir()?; + let a_dir = tmp.path().join("Props"); + let b_dir = tmp.path().join("Materials"); + std::fs::create_dir_all(&a_dir)?; + std::fs::create_dir_all(&b_dir)?; + let a = a_dir.join("link.usd"); + let b = b_dir.join("Materials.usd"); + std::fs::write(&a, b"placeholder")?; + std::fs::write(&b, b"placeholder")?; + let identifiers = vec![ + a.canonicalize()?.to_string_lossy().into_owned(), + b.canonicalize()?.to_string_lossy().into_owned(), + ]; + let resolver = DefaultResolver::new(); + // `../Materials/Materials.usd` written inside `Props/link.usd` + // should resolve to identifier index 1 (the Materials.usd). + assert_eq!( + find_layer("../Materials/Materials.usd", &identifiers, &resolver), + Some(1) + ); Ok(()) } @@ -1570,7 +1635,7 @@ mod tests { "should have node from C.usd via nested reference" ); - let a_idx = find_layer("A.usd", &stack.identifiers).unwrap(); + let a_idx = find_layer("A.usd", &stack.identifiers, &*stack.resolver).unwrap(); let a_attr_path = Path::new("/A.A_attr").unwrap(); assert!( stack.layer(a_idx).has_spec(&a_attr_path), @@ -1616,7 +1681,7 @@ mod tests { let stack = load_stack(&path)?; assert!( - find_layer("camera_perspective.usd", &stack.identifiers).is_some(), + find_layer("camera_perspective.usd", &stack.identifiers, &*stack.resolver).is_some(), "camera_perspective.usd should be collected from variant reference" ); @@ -1688,7 +1753,7 @@ def "Root" ( ); let layers = vec![a, b]; let ids = vec!["a.usd".to_string(), "b.usd".to_string()]; - let stack = LayerStack::new(layers, ids, 0); + let stack = LayerStack::new(layers, ids, 0, Box::new(DefaultResolver::new())); let result = PrimIndex::build_with_context(&Path::from("/Root"), &stack, &CompositionContext::default()); assert!( @@ -1712,7 +1777,7 @@ def "Prim" ( ); let layers = vec![layer]; let ids = vec!["test.usda".to_string()]; - let stack = LayerStack::new(layers, ids, 0); + let stack = LayerStack::new(layers, ids, 0, Box::new(DefaultResolver::new())); let result = PrimIndex::build_with_context(&Path::from("/Prim"), &stack, &CompositionContext::default()); assert!( @@ -1738,7 +1803,7 @@ def "Prim" ( let target = parse_usda("#usda 1.0\ndef \"Foo\" {}\n"); let layers = vec![root, target]; let ids = vec!["root.usda".to_string(), "target.usda".to_string()]; - let stack = LayerStack::new(layers, ids, 0); + let stack = LayerStack::new(layers, ids, 0, Box::new(DefaultResolver::new())); let result = PrimIndex::build_with_context(&Path::from("/Prim"), &stack, &CompositionContext::default()); assert!( diff --git a/src/pcp/mod.rs b/src/pcp/mod.rs index 90cf7ad..65ea852 100644 --- a/src/pcp/mod.rs +++ b/src/pcp/mod.rs @@ -101,6 +101,7 @@ mod rel; use std::collections::{HashMap, HashSet, VecDeque}; +use crate::ar::Resolver; use crate::sdf::schema::FieldKey; use crate::sdf::{self, LayerData, Path, Value}; @@ -183,13 +184,20 @@ pub(crate) struct LayerStack { /// stack that contains it. Precomputed from `sublayer_stacks` to keep /// per-prim composition off the linear-scan hot path. layer_offsets: HashMap, + /// Resolver used to anchor relative asset paths when locating layers. + pub(crate) resolver: Box, } impl LayerStack { /// Creates a new layer stack, precomputing sublayer ordering. - pub fn new(layers: Vec, identifiers: Vec, session_layer_count: usize) -> Self { + pub fn new( + layers: Vec, + identifiers: Vec, + session_layer_count: usize, + resolver: Box, + ) -> Self { let sublayer_stacks: SublayerStacks = (0..layers.len()) - .map(|i| (i, Self::build_sublayer_stack(i, &layers, &identifiers))) + .map(|i| (i, Self::build_sublayer_stack(i, &layers, &identifiers, &*resolver))) .collect(); let mut layer_offsets: HashMap = HashMap::new(); for stack in sublayer_stacks.values() { @@ -203,6 +211,7 @@ impl LayerStack { sublayer_stacks, session_layer_count, layer_offsets, + resolver, } } @@ -248,6 +257,7 @@ impl LayerStack { root_layer: usize, layers: &[LayerData], identifiers: &[String], + resolver: &dyn Resolver, ) -> Vec<(usize, sdf::LayerOffset)> { let mut stack: Vec<(usize, sdf::LayerOffset)> = vec![(root_layer, sdf::LayerOffset::IDENTITY)]; let mut seen: HashSet = HashSet::new(); @@ -277,7 +287,7 @@ impl LayerStack { .unwrap_or_default(); for (i, sub_path) in sub_paths.into_iter().enumerate() { - let Some(sub_idx) = index::find_layer(&sub_path, identifiers) else { + let Some(sub_idx) = index::find_layer(&sub_path, identifiers, resolver) else { continue; }; if !seen.insert(sub_idx) { diff --git a/src/stage.rs b/src/stage.rs index 7704541..78b9299 100644 --- a/src/stage.rs +++ b/src/stage.rs @@ -87,7 +87,8 @@ impl Stage { /// /// `session_layers` are prepended at the front of the layer stack so they /// hold the strongest opinions (stronger than the root layer). - fn from_layers( + fn from_layers( + resolver: R, session_layers: Vec, root_layers: Vec, on_composition_error: Box Result<()>>, @@ -103,7 +104,7 @@ impl Stage { layers.push(layer.data); } - let stack = pcp::LayerStack::new(layers, identifiers, session_layer_count); + let stack = pcp::LayerStack::new(layers, identifiers, session_layer_count, Box::new(resolver)); Self { graph: RefCell::new(pcp::Cache::new(stack, variant_fallbacks)), on_composition_error, @@ -362,6 +363,7 @@ impl Result<()>> StageBuilder { /// Opens a stage from a root layer file. pub fn open(self, root_path: &str) -> Result where + R: 'static, E: 'static, { let on_error = self.on_error; @@ -379,6 +381,7 @@ impl Result<()>> StageBuilder { let pcp_handler = Box::new(move |e: pcp::Error| on_error(CompositionError::Pcp(e))); Ok(Stage::from_layers( + self.resolver, session_layers, root_layers, pcp_handler,