Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 80 additions & 15 deletions src/pcp/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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,
)
})
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -1323,12 +1330,19 @@ fn collect_payloads_in(nodes: &[Node], layers: &[LayerData]) -> Vec<Payload> {

/// 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<usize> {
/// 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<usize> {
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);
Expand All @@ -1342,6 +1356,19 @@ pub(super) fn find_layer(asset_path: &str, identifiers: &[String]) -> Option<usi
}
}

// Relative paths traversing parent directories need anchoring. Delegate to
// the resolver so custom AR backends override path handling without any
// filesystem access here.
if needle.starts_with("../") || needle.starts_with("..\\") {
for anchor_id in identifiers {
let anchor = ResolvedPath::new(PathBuf::from(anchor_id));
let resolved = resolver.create_identifier(asset_path, Some(&anchor));
if let Some(pos) = identifiers.iter().position(|id| *id == resolved) {
return Some(pos);
}
}
}

None
}

Expand Down Expand Up @@ -1411,7 +1438,12 @@ mod tests {
/// Helper: loads layers and creates a [`LayerStack`].
fn load_stack(path: &str) -> anyhow::Result<LayerStack> {
let (layers, identifiers) = load_layers(path)?;
Ok(LayerStack::new(layers, identifiers, 0))
Ok(LayerStack::new(
layers,
identifiers,
0,
Box::new(DefaultResolver::new()),
))
}

#[test]
Expand Down Expand Up @@ -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(())
}

Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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"
);

Expand Down Expand Up @@ -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!(
Expand All @@ -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!(
Expand All @@ -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!(
Expand Down
16 changes: 13 additions & 3 deletions src/pcp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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<usize, sdf::LayerOffset>,
/// Resolver used to anchor relative asset paths when locating layers.
pub(crate) resolver: Box<dyn Resolver>,
}

impl LayerStack {
/// Creates a new layer stack, precomputing sublayer ordering.
pub fn new(layers: Vec<LayerData>, identifiers: Vec<String>, session_layer_count: usize) -> Self {
pub fn new(
layers: Vec<LayerData>,
identifiers: Vec<String>,
session_layer_count: usize,
resolver: Box<dyn Resolver>,
) -> 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<usize, sdf::LayerOffset> = HashMap::new();
for stack in sublayer_stacks.values() {
Expand All @@ -203,6 +211,7 @@ impl LayerStack {
sublayer_stacks,
session_layer_count,
layer_offsets,
resolver,
}
}

Expand Down Expand Up @@ -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<usize> = HashSet::new();
Expand Down Expand Up @@ -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) {
Expand Down
7 changes: 5 additions & 2 deletions src/stage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<R: Resolver + 'static>(
resolver: R,
session_layers: Vec<layer::Layer>,
root_layers: Vec<layer::Layer>,
on_composition_error: Box<dyn Fn(pcp::Error) -> Result<()>>,
Expand All @@ -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,
Expand Down Expand Up @@ -362,6 +363,7 @@ impl<R: Resolver, E: Fn(CompositionError) -> Result<()>> StageBuilder<R, E> {
/// Opens a stage from a root layer file.
pub fn open(self, root_path: &str) -> Result<Stage>
where
R: 'static,
E: 'static,
{
let on_error = self.on_error;
Expand All @@ -379,6 +381,7 @@ impl<R: Resolver, E: Fn(CompositionError) -> Result<()>> StageBuilder<R, E> {

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,
Expand Down