use std::collections::HashMap;
use std::fmt;
use std::io::{self, Cursor, Write};
use std::path::Path as FsPath;
use std::rc::Rc;
use crate::{ar, sdf, tf};
use super::schema::FieldKey;
use super::{
AbstractData, AttributeSpecMut, AttributeSpecRef, ChangeList, CowData, Data, DataError, FormatError, IntoPath,
LayerData, Patch, Path, PathParseError, PrimSpecMut, PrimSpecRef, PseudoRootSpecMut, PseudoRootSpecRef,
RelationshipSpecMut, RelationshipSpecRef, RelocateList, SpecError, SpecType, Value, ValueTypeError, sink,
try_into_path,
};
pub type LayerSinkId = sink::Id<dyn LayerSink>;
const ANONYMOUS_PREFIX: &str = "anon:";
pub struct Layer {
pub identifier: String,
real_path: Option<String>,
data: CowData<LayerData>,
changes: ChangeList,
sinks: sink::Set<dyn LayerSink>,
resolver: Option<Rc<dyn ar::Resolver>>,
}
impl Layer {
pub(crate) fn new(identifier: impl Into<String>, data: LayerData) -> Self {
Self::build(identifier.into(), None, data)
}
pub fn open(identifier: impl AsRef<str>) -> crate::Result<Self> {
Self::open_with(ar::DefaultResolver::new(), identifier)
}
pub fn open_with(resolver: impl ar::Resolver + 'static, identifier: impl AsRef<str>) -> crate::Result<Self> {
let registry = sdf::LayerRegistry::new(Box::new(resolver));
let identifier = registry.create_identifier(identifier.as_ref(), None);
let layer = registry.open_layer(&identifier)?;
layer.ok_or(crate::Error::UnresolvedAsset(identifier))
}
pub fn from_bytes(tag: impl fmt::Display, bytes: impl Into<ar::AssetBuffer>) -> Result<Self, FormatError> {
let identifier = Self::anonymous_identifier(tag);
let data = sdf::LayerRegistry::read_bytes(bytes.into(), &identifier)?;
Ok(Self::new(identifier, data))
}
pub(crate) fn new_resolved(
identifier: impl Into<String>,
real_path: &ar::ResolvedPath,
data: LayerData,
resolver: Rc<dyn ar::Resolver>,
) -> Self {
let identifier = identifier.into();
let real_path = real_path.to_string();
let real_path = (real_path != identifier).then_some(real_path);
let mut layer = Self::build(identifier, real_path, data);
layer.resolver = Some(resolver);
layer
}
fn build(identifier: String, real_path: Option<String>, data: LayerData) -> Self {
Self {
identifier,
real_path,
data: CowData::new(data),
changes: ChangeList::new(),
sinks: sink::Set::default(),
resolver: None,
}
}
pub fn add_sink<S: LayerSink + 'static>(&mut self, sink: S) -> LayerSinkId {
self.sinks.add(Box::new(sink))
}
pub fn remove_sink(&mut self, id: LayerSinkId) {
self.sinks.remove(id);
}
pub fn data(&self) -> &dyn AbstractData {
&self.data
}
fn rollback(&mut self) {
self.data.rollback();
}
pub fn edit(
&mut self,
f: impl FnOnce(&mut LayerEdit<'_>) -> Result<(), AuthoringError>,
) -> Result<bool, EditError> {
edit_layers(&mut [self], |edits| f(&mut edits[0]).map_err(EditError::from))
}
fn prepare_commit(&mut self, generation: u64) -> Result<(), sink::Error> {
self.changes.update(&self.data);
if !self.changes.is_empty() && !self.sinks.is_empty() {
let change = PendingLayerChange {
layer_identifier: &self.identifier,
base: &**self.data.base(),
overlay: self.data.overlay(),
change_list: &self.changes,
generation,
};
self.sinks.iter().try_for_each(|sink| sink.before_commit(&change))?;
}
Ok(())
}
fn commit_data(&mut self) -> bool {
self.data.commit();
!self.changes.is_empty()
}
fn notify_after_commit(&self) {
if !self.changes.is_empty() {
for sink in self.sinks.iter() {
sink.after_commit(&self.identifier, &self.changes);
}
}
}
pub fn is_dirty(&self) -> bool {
!self.data.is_empty()
}
pub(crate) fn discard_changes(&mut self) {
self.data.commit();
}
pub fn identifier(&self) -> &str {
&self.identifier
}
pub fn resolved_path(&self) -> Option<&str> {
(!self.is_anonymous()).then(|| self.real_path())
}
pub(crate) fn real_path(&self) -> &str {
self.real_path.as_deref().unwrap_or(&self.identifier)
}
pub(crate) fn anchor_location(&self) -> Option<ar::ResolvedPath> {
(!self.is_anonymous()).then(|| ar::ResolvedPath::new(self.real_path()))
}
}
impl Layer {
pub fn export(&self, filename: impl AsRef<str>) -> Result<(), ExportError> {
let filename = filename.as_ref();
let format = super::LayerRegistry::export_format(filename)?;
let mut temp = tf::SafeOutputFile::create(FsPath::new(filename)).map_err(|source| ExportError::Create {
filename: filename.to_owned(),
source,
})?;
format.write(self.data(), temp.file())?;
temp.persist().map_err(|source| ExportError::Replace {
filename: filename.to_owned(),
source,
})
}
pub fn export_to_string(&self) -> Result<String, ExportError> {
let format = super::LayerRegistry::find_by_id("usda").expect("usda is a built-in format");
let mut buf = Cursor::new(Vec::new());
format.write(self.data(), &mut buf)?;
Ok(String::from_utf8(buf.into_inner()).expect("the usda writer emits UTF-8 text"))
}
pub fn save(&mut self) -> Result<(), ExportError> {
if self.is_anonymous() {
return Err(ExportError::Anonymous {
identifier: self.identifier.clone(),
});
}
let path = FsPath::new(&self.identifier);
if !path.is_absolute() {
return Err(ExportError::NotAFilePath {
identifier: self.identifier.clone(),
});
}
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or_default();
if let Some(format) = super::LayerRegistry::find_by_extension(ext)
&& !format.caps().can_edit()
{
return Err(ExportError::NotEditable {
identifier: self.identifier.clone(),
format: format.format_id(),
});
}
let format = super::LayerRegistry::export_format(&self.identifier)?;
let written = format.write_bytes(self.data())?;
let create = |source| ExportError::Create {
filename: self.identifier.clone(),
source,
};
let mut temp = tf::SafeOutputFile::create(path).map_err(create)?;
temp.file().write_all(&written.bytes).map_err(create)?;
let replaced = temp.persist().map_err(|source| ExportError::Replace {
filename: self.identifier.clone(),
source,
});
if let Some(serialized) = written.data {
let reopened = match &replaced {
Ok(()) => self.reopen_saved(&written.bytes, format),
Err(_) => None,
};
self.data = CowData::new(reopened.unwrap_or(serialized));
}
replaced
}
fn reopen_saved(&self, written: &ar::SharedBuffer, format: &dyn super::FileFormat) -> Option<LayerData> {
let resolved = ar::ResolvedPath::new(self.real_path());
let saved = self.resolver.as_ref()?.open_asset(&resolved).ok()?.shared_buffer()?;
if !saved.is_mapping() || saved[..] != written[..] {
return None;
}
format
.read_bytes(ar::AssetBuffer::Shared(saved), &resolved.to_string())
.ok()
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ExportError {
#[error("layer path {filename} has no extension; cannot choose format")]
NoExtension {
filename: String,
},
#[error("unsupported layer extension {extension:?} (expected usda/usdc/usd/usdz)")]
UnsupportedExtension {
extension: String,
},
#[error("file format {format} does not support writing")]
NotWritable {
format: tf::Token,
},
#[error("cannot save anonymous layer {identifier}")]
Anonymous {
identifier: String,
},
#[error(
"cannot save layer {identifier}: identifier is not an absolute file path; \
use Layer::export(path) to choose a destination"
)]
NotAFilePath {
identifier: String,
},
#[error(
"cannot save layer {identifier} in place: the {format} format is not editable; \
use Layer::export(path) to write a new copy"
)]
NotEditable {
identifier: String,
format: tf::Token,
},
#[error("failed to create {filename}")]
Create {
filename: String,
#[source]
source: io::Error,
},
#[error("failed to replace {filename}")]
Replace {
filename: String,
#[source]
source: io::Error,
},
#[error(transparent)]
Format(#[from] FormatError),
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum AuthoringError {
#[error(transparent)]
Parse(#[from] PathParseError),
#[error(transparent)]
Spec(#[from] SpecError),
#[error(transparent)]
Data(#[from] DataError),
#[error(transparent)]
ValueType(#[from] ValueTypeError),
#[error("path {path} is not valid here: {reason}")]
InvalidPath {
path: Path,
reason: &'static str,
},
}
#[derive(Debug, thiserror::Error)]
pub enum EditError {
#[error(transparent)]
Author(#[from] AuthoringError),
#[error(transparent)]
Rejected(#[from] sink::Error),
}
pub trait LayerSink {
fn before_commit(&self, change: &PendingLayerChange<'_>) -> Result<(), sink::Error> {
let _ = change;
Ok(())
}
fn after_commit(&self, layer: &str, changes: &ChangeList) {
let _ = (layer, changes);
}
}
impl<F: Fn(&str, &ChangeList)> LayerSink for F {
fn after_commit(&self, layer: &str, changes: &ChangeList) {
self(layer, changes);
}
}
pub struct PendingLayerChange<'a> {
pub layer_identifier: &'a str,
pub base: &'a dyn AbstractData,
pub overlay: &'a HashMap<Path, Patch>,
pub change_list: &'a ChangeList,
pub generation: u64,
}
impl Layer {
pub fn new_anonymous(tag: impl fmt::Display) -> Self {
Self::new_in_memory(Self::anonymous_identifier(tag))
}
pub(crate) fn anonymous_identifier(tag: impl fmt::Display) -> String {
let n = crate::next_id();
format!("{ANONYMOUS_PREFIX}{n}:{tag}")
}
pub(crate) fn new_in_memory(identifier: impl Into<String>) -> Self {
let mut data = Data::new();
data.create_spec(Path::abs_root(), SpecType::PseudoRoot);
Self::new(identifier, Box::new(data))
}
pub fn is_anonymous(&self) -> bool {
Self::is_anonymous_identifier(&self.identifier)
}
pub fn is_anonymous_identifier(identifier: &str) -> bool {
identifier.starts_with(ANONYMOUS_PREFIX)
}
pub fn prim(&self, path: impl IntoPath) -> Result<Option<PrimSpecRef<'_>>, PathParseError> {
Ok(PrimSpecRef::get(self.data(), try_into_path(path)?))
}
pub fn attribute(&self, path: impl IntoPath) -> Result<Option<AttributeSpecRef<'_>>, PathParseError> {
Ok(AttributeSpecRef::get(self.data(), try_into_path(path)?))
}
pub fn relationship(&self, path: impl IntoPath) -> Result<Option<RelationshipSpecRef<'_>>, PathParseError> {
Ok(RelationshipSpecRef::get(self.data(), try_into_path(path)?))
}
pub fn pseudo_root(&self) -> Option<PseudoRootSpecRef<'_>> {
PseudoRootSpecRef::get(self.data())
}
pub fn relocates(&self) -> RelocateList {
self.pseudo_root().and_then(|root| root.relocates()).unwrap_or_default()
}
pub fn has_relocates(&self) -> bool {
self.has_root_field(FieldKey::LayerRelocates)
}
pub fn start_time_code(&self) -> f64 {
self.pseudo_root()
.and_then(|root| root.start_time_code())
.unwrap_or(0.0)
}
pub fn has_start_time_code(&self) -> bool {
self.has_root_field(FieldKey::StartTimeCode)
}
pub fn end_time_code(&self) -> f64 {
self.pseudo_root().and_then(|root| root.end_time_code()).unwrap_or(0.0)
}
pub fn has_end_time_code(&self) -> bool {
self.has_root_field(FieldKey::EndTimeCode)
}
pub fn time_codes_per_second(&self) -> f64 {
let root = self.pseudo_root();
root.as_ref()
.and_then(|root| root.time_codes_per_second())
.or_else(|| root.as_ref().and_then(|root| root.frames_per_second()))
.unwrap_or(24.0)
}
pub fn has_time_codes_per_second(&self) -> bool {
self.has_root_field(FieldKey::TimeCodesPerSecond)
}
pub fn frames_per_second(&self) -> f64 {
self.pseudo_root()
.and_then(|root| root.frames_per_second())
.unwrap_or(24.0)
}
pub fn has_frames_per_second(&self) -> bool {
self.has_root_field(FieldKey::FramesPerSecond)
}
pub fn frame_precision(&self) -> i32 {
self.pseudo_root().and_then(|root| root.frame_precision()).unwrap_or(3)
}
pub fn has_frame_precision(&self) -> bool {
self.has_root_field(FieldKey::FramePrecision)
}
fn has_root_field(&self, key: FieldKey) -> bool {
self.data.has_field(&Path::abs_root(), key.as_str())
}
}
pub struct LayerEdit<'a> {
layer: &'a mut Layer,
}
impl std::ops::Deref for LayerEdit<'_> {
type Target = Layer;
fn deref(&self) -> &Layer {
&*self.layer
}
}
impl LayerEdit<'_> {
pub fn data_mut(&mut self) -> &mut dyn AbstractData {
&mut self.layer.data
}
pub fn remove_spec(&mut self, path: &Path) -> Result<bool, AuthoringError> {
super::spec::remove_spec(self.data_mut(), path)
}
pub fn prim_mut(&mut self, path: impl IntoPath) -> Result<Option<PrimSpecMut<'_>>, PathParseError> {
Ok(PrimSpecMut::get(self.data_mut(), try_into_path(path)?))
}
pub fn attribute_mut(&mut self, path: impl IntoPath) -> Result<Option<AttributeSpecMut<'_>>, PathParseError> {
Ok(AttributeSpecMut::get(self.data_mut(), try_into_path(path)?))
}
pub fn relationship_mut(&mut self, path: impl IntoPath) -> Result<Option<RelationshipSpecMut<'_>>, PathParseError> {
Ok(RelationshipSpecMut::get(self.data_mut(), try_into_path(path)?))
}
pub fn pseudo_root_mut(&mut self) -> Result<PseudoRootSpecMut<'_>, AuthoringError> {
let root = Path::abs_root();
match self.layer.data().spec_type(&root) {
Some(SpecType::PseudoRoot) => {}
Some(_) => {
return Err(AuthoringError::InvalidPath {
path: root,
reason: "root spec exists with non-PseudoRoot SpecType",
});
}
None => self.data_mut().create_spec(root, SpecType::PseudoRoot),
}
Ok(PseudoRootSpecMut::get(self.data_mut()).expect("just ensured a pseudo-root spec"))
}
pub fn set_default_prim(&mut self, name: impl Into<String>) -> Result<(), AuthoringError> {
let name = name.into();
if default_prim_path(&name).is_none() {
return Err(AuthoringError::InvalidPath {
path: Path::abs_root(),
reason: "defaultPrim must name a prim",
});
}
self.pseudo_root_mut()?.set_default_prim(name);
Ok(())
}
pub fn clear_default_prim(&mut self) -> Result<(), AuthoringError> {
self.clear_root_field(FieldKey::DefaultPrim)
}
pub fn set_relocates(&mut self, relocates: RelocateList) -> Result<(), AuthoringError> {
self.pseudo_root_mut()?.set_relocates(relocates);
Ok(())
}
pub fn clear_relocates(&mut self) -> Result<(), AuthoringError> {
self.clear_root_field(FieldKey::LayerRelocates)
}
pub fn set_expression_variables(&mut self, vars: HashMap<String, Value>) -> Result<(), AuthoringError> {
self.pseudo_root_mut()?.set_expression_variables(vars);
Ok(())
}
pub fn clear_expression_variables(&mut self) -> Result<(), AuthoringError> {
self.clear_root_field(FieldKey::ExpressionVariables)
}
pub fn set_start_time_code(&mut self, time: f64) -> Result<(), AuthoringError> {
self.pseudo_root_mut()?.set_start_time_code(time);
Ok(())
}
pub fn clear_start_time_code(&mut self) -> Result<(), AuthoringError> {
self.clear_root_field(FieldKey::StartTimeCode)
}
pub fn set_end_time_code(&mut self, time: f64) -> Result<(), AuthoringError> {
self.pseudo_root_mut()?.set_end_time_code(time);
Ok(())
}
pub fn clear_end_time_code(&mut self) -> Result<(), AuthoringError> {
self.clear_root_field(FieldKey::EndTimeCode)
}
pub fn set_time_codes_per_second(&mut self, rate: f64) -> Result<(), AuthoringError> {
self.pseudo_root_mut()?.set_time_codes_per_second(rate);
Ok(())
}
pub fn clear_time_codes_per_second(&mut self) -> Result<(), AuthoringError> {
self.clear_root_field(FieldKey::TimeCodesPerSecond)
}
pub fn set_frames_per_second(&mut self, rate: f64) -> Result<(), AuthoringError> {
self.pseudo_root_mut()?.set_frames_per_second(rate);
Ok(())
}
pub fn clear_frames_per_second(&mut self) -> Result<(), AuthoringError> {
self.clear_root_field(FieldKey::FramesPerSecond)
}
pub fn set_frame_precision(&mut self, precision: i32) -> Result<(), AuthoringError> {
self.pseudo_root_mut()?.set_frame_precision(precision);
Ok(())
}
pub fn clear_frame_precision(&mut self) -> Result<(), AuthoringError> {
self.clear_root_field(FieldKey::FramePrecision)
}
fn clear_root_field(&mut self, key: FieldKey) -> Result<(), AuthoringError> {
self.data_mut().erase_field(&Path::abs_root(), key.as_str());
Ok(())
}
}
impl std::fmt::Debug for Layer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Layer")
.field("identifier", &self.identifier)
.finish_non_exhaustive()
}
}
pub fn default_prim_path(token: &str) -> Option<Path> {
let path = Path::new(token).ok()?;
if !path.is_prim_path() || path.as_str().starts_with('.') {
return None;
}
Some(Path::abs_root().make_absolute(&path))
}
pub(crate) fn edit_layers<E: From<sink::Error>>(
layers: &mut [&mut Layer],
f: impl FnOnce(&mut [LayerEdit<'_>]) -> Result<(), E>,
) -> Result<bool, E> {
let mut guard = GroupEditGuard {
layers,
committed: false,
};
{
let mut edits: Vec<LayerEdit<'_>> = guard.layers.iter_mut().map(|layer| LayerEdit { layer }).collect();
f(&mut edits)?;
}
let generation = crate::next_id();
for layer in guard.layers.iter_mut() {
layer.prepare_commit(generation)?;
}
let mut changed = false;
for layer in guard.layers.iter_mut() {
changed |= layer.commit_data();
}
guard.committed = true;
for layer in guard.layers.iter() {
layer.notify_after_commit();
}
Ok(changed)
}
pub(crate) fn dry_run_layers<E>(
layers: &mut [&mut Layer],
f: impl FnOnce(&mut [LayerEdit<'_>]) -> Result<(), E>,
) -> Result<(), E> {
let guard = GroupEditGuard {
layers,
committed: false,
};
let mut edits: Vec<LayerEdit<'_>> = guard.layers.iter_mut().map(|layer| LayerEdit { layer }).collect();
f(&mut edits)
}
struct GroupEditGuard<'a, 'b> {
layers: &'b mut [&'a mut Layer],
committed: bool,
}
impl Drop for GroupEditGuard<'_, '_> {
fn drop(&mut self) {
if self.committed {
return;
}
for layer in self.layers.iter_mut() {
layer.rollback();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Result;
use std::cell::{Cell, RefCell};
use std::fs;
use std::io;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use crate::sdf;
use crate::usda;
#[test]
fn open_canonicalizes_identifier() -> Result<()> {
let dir = tempfile::tempdir()?;
let path = dir.path().join("scene.usda");
std::fs::write(&path, "#usda 1.0\n")?;
let indirect = dir.path().join(".").join("scene.usda");
let mut layer = Layer::open(indirect.to_string_lossy())?;
assert_eq!(
layer.identifier(),
path.canonicalize()?.to_string_lossy(),
"the identifier is the canonical one, not the spelling passed in"
);
layer
.save()
.expect("a layer that loaded from a file can save back to it");
Ok(())
}
fn edit_layer(layer: &mut Layer, f: impl FnOnce(&mut LayerEdit<'_>)) {
layer
.edit(|e| {
f(e);
Ok(())
})
.expect("authored");
}
fn crate_fixture(dir: &FsPath) -> Result<PathBuf> {
let path = dir.join("scene.usdc");
let text = format!(
"#usda 1.0\ndef \"World\"\n{{\n float[] size = [{}]\n}}\n",
["1"; 64].join(", ")
);
Layer::from_bytes("scene", text.into_bytes())?.export(path.to_string_lossy())?;
Ok(path)
}
fn world_kind(layer: &Layer) -> Option<tf::Token> {
layer.prim("/World").unwrap().expect("the prim exists").kind()
}
fn set_component(layer: &mut Layer) {
edit_layer(layer, |e| {
e.prim_mut("/World")
.unwrap()
.expect("the prim exists")
.set_kind("component");
});
}
#[test]
fn save_crate_over_itself() -> Result<()> {
let dir = tempfile::tempdir()?;
let path = crate_fixture(dir.path())?;
let mut layer = Layer::open(path.to_string_lossy())?;
set_component(&mut layer);
layer.save()?;
let component = Some(tf::Token::from("component"));
assert_eq!(world_kind(&layer), component);
let size = layer
.data()
.get_field(&Path::new("/World.size")?, "default")
.expect("decoded on demand")
.into_owned();
assert_eq!(size, Value::FloatVec(vec![1.0; 64]));
assert_eq!(world_kind(&Layer::open(path.to_string_lossy())?), component);
assert_eq!(fs::read_dir(dir.path())?.count(), 1, "no temporary file remains");
Ok(())
}
#[test]
fn replace_failure_keeps_edits() -> Result<()> {
let dir = tempfile::tempdir()?;
let path = crate_fixture(dir.path())?;
let mut layer = Layer::open(path.to_string_lossy())?;
set_component(&mut layer);
fs::remove_file(&path)?;
fs::create_dir(&path)?;
let error = layer.save().expect_err("the replacement fails");
assert!(matches!(error, ExportError::Replace { .. }), "{error:?}");
let component = Some(tf::Token::from("component"));
assert_eq!(world_kind(&layer), component, "the edit survives in memory");
assert_eq!(fs::read_dir(dir.path())?.count(), 1, "no temporary file remains");
fs::remove_dir(&path)?;
layer.save()?;
assert_eq!(world_kind(&Layer::open(path.to_string_lossy())?), component);
Ok(())
}
#[cfg(unix)]
#[test]
fn export_through_symlink() -> Result<()> {
use std::os::unix::fs::symlink;
let dir = tempfile::tempdir()?;
let target = crate_fixture(dir.path())?;
let link = dir.path().join("link.usdc");
symlink(&target, &link)?;
let mut layer = Layer::open(target.to_string_lossy())?;
set_component(&mut layer);
layer.export(link.to_string_lossy())?;
assert!(
fs::symlink_metadata(&link)?.file_type().is_symlink(),
"the link is kept"
);
let component = Some(tf::Token::from("component"));
assert_eq!(world_kind(&Layer::open(target.to_string_lossy())?), component);
Ok(())
}
#[cfg(windows)]
#[test]
fn save_keeps_acl() -> Result<()> {
use std::env;
use std::process::Command;
let dir = tempfile::tempdir()?;
let path = crate_fixture(dir.path())?;
let file = path.to_str().expect("utf-8 temp path");
let icacls = |args: &[&str]| -> Result<String> {
let output = Command::new("icacls").args(args).output()?;
assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
};
let grant = format!("{}:(R,W)", env::var("USERNAME").expect("USERNAME is set"));
icacls(&[file, "/inheritance:r", "/grant:r", &grant])?;
let before = icacls(&[file])?;
assert!(before.contains("(R,W)"), "{before}");
let mut layer = Layer::open(file)?;
set_component(&mut layer);
layer.save()?;
assert_eq!(icacls(&[file])?, before, "the list survives the save");
layer.export(file)?;
assert_eq!(icacls(&[file])?, before, "the list survives the export");
assert_eq!(world_kind(&Layer::open(file)?), Some(tf::Token::from("component")));
Ok(())
}
#[cfg(unix)]
#[test]
fn save_keeps_permissions() -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir()?;
let path = crate_fixture(dir.path())?;
fs::set_permissions(&path, fs::Permissions::from_mode(0o600))?;
let mut layer = Layer::open(path.to_string_lossy())?;
layer.save()?;
assert_eq!(fs::metadata(&path)?.permissions().mode() & 0o777, 0o600);
layer.export(path.to_string_lossy())?;
assert_eq!(fs::metadata(&path)?.permissions().mode() & 0o777, 0o600);
Ok(())
}
#[test]
fn export_leaves_no_temp() -> Result<()> {
let dir = tempfile::tempdir()?;
let path = crate_fixture(dir.path())?;
let layer = Layer::open(path.to_string_lossy())?;
layer.export(path.to_string_lossy())?;
assert_eq!(fs::read_dir(dir.path())?.count(), 1);
let missing = dir.path().join("missing").join("scene.usdc");
let error = layer.export(missing.to_string_lossy()).expect_err("no such directory");
assert!(matches!(error, ExportError::Create { .. }), "{error:?}");
assert_eq!(fs::read_dir(dir.path())?.count(), 1);
Ok(())
}
#[cfg(feature = "mmap")]
#[test]
#[allow(unsafe_code)]
fn save_with_other_view() -> Result<()> {
let dir = tempfile::tempdir()?;
let path = crate_fixture(dir.path())?;
let resolver = unsafe { ar::DefaultResolver::new().map_files() };
let registry = crate::sdf::LayerRegistry::new(Box::new(resolver));
let identifier = registry.create_identifier(&path.to_string_lossy(), None);
let mut editing = registry.open_layer(&identifier)?.expect("the layer resolves");
let other = registry.open_layer(&identifier)?.expect("the layer resolves");
set_component(&mut editing);
editing.save()?;
let component = Some(tf::Token::from("component"));
assert_eq!(world_kind(&editing), component);
assert_eq!(world_kind(&other), None, "the other view reads the file it mapped");
let size = other
.data()
.get_field(&Path::new("/World.size")?, "default")
.expect("still decodes from its view")
.into_owned();
assert_eq!(size, Value::FloatVec(vec![1.0; 64]));
assert_eq!(world_kind(&Layer::open(path.to_string_lossy())?), component);
Ok(())
}
#[test]
fn anonymous_identifiers_unique() {
let a = Layer::new_anonymous("scene.usda");
let b = Layer::new_anonymous("scene.usda");
assert_ne!(a.identifier(), b.identifier());
assert!(a.identifier().ends_with(":scene.usda"));
assert!(a.is_anonymous() && b.is_anonymous());
assert!(!Layer::new_in_memory("scene.usda").is_anonymous());
}
#[test]
fn relocates_round_trip() {
let mut layer = Layer::new_anonymous("test.usda");
assert!(!layer.has_relocates());
assert!(layer.relocates().is_empty());
let pairs = vec![
(Path::new("/Rig/Model").unwrap(), Path::new("/Group/Model").unwrap()),
(Path::new("/Rig/Dead").unwrap(), Path::default()),
];
edit_layer(&mut layer, |e| {
e.set_relocates(pairs.clone()).unwrap();
});
assert!(layer.has_relocates());
assert_eq!(layer.relocates(), pairs);
}
#[test]
fn empty_opinion_vs_cleared() {
let mut layer = Layer::new_anonymous("test.usda");
edit_layer(&mut layer, |e| {
e.set_relocates(RelocateList::new()).unwrap();
});
assert!(layer.has_relocates());
assert!(layer.relocates().is_empty());
edit_layer(&mut layer, |e| {
e.clear_relocates().unwrap();
});
assert!(!layer.has_relocates());
assert!(layer.relocates().is_empty());
}
#[test]
fn time_code_round_trip() {
let mut layer = Layer::new_anonymous("test.usda");
assert!(!layer.has_start_time_code());
assert_eq!(layer.start_time_code(), 0.0);
assert_eq!(layer.end_time_code(), 0.0);
assert_eq!(layer.frame_precision(), 3);
edit_layer(&mut layer, |e| {
e.set_start_time_code(1.0).unwrap();
e.set_end_time_code(48.0).unwrap();
e.set_frame_precision(6).unwrap();
});
assert!(layer.has_start_time_code());
assert_eq!(layer.start_time_code(), 1.0);
assert_eq!(layer.end_time_code(), 48.0);
assert_eq!(layer.frame_precision(), 6);
}
#[test]
fn tcps_fps_fallback() {
let mut layer = Layer::new_anonymous("test.usda");
assert_eq!(layer.time_codes_per_second(), 24.0);
assert_eq!(layer.frames_per_second(), 24.0);
edit_layer(&mut layer, |e| {
e.set_frames_per_second(30.0).unwrap();
});
assert_eq!(layer.time_codes_per_second(), 30.0);
edit_layer(&mut layer, |e| {
e.set_time_codes_per_second(48.0).unwrap();
});
assert_eq!(layer.time_codes_per_second(), 48.0);
}
#[test]
fn clear_time_code() {
let mut layer = Layer::new_anonymous("test.usda");
edit_layer(&mut layer, |e| {
e.set_start_time_code(5.0).unwrap();
});
assert!(layer.has_start_time_code());
edit_layer(&mut layer, |e| {
e.clear_start_time_code().unwrap();
});
assert!(!layer.has_start_time_code());
assert_eq!(layer.start_time_code(), 0.0);
}
#[test]
fn edit_commits() {
use crate::sdf::{PrimSpec, Specifier};
let mut layer = Layer::new_anonymous("test.usda");
let changed = layer
.edit(|l| {
PrimSpec::new(l.data_mut(), "/World", Specifier::Def, "Xform")?;
Ok(())
})
.expect("no error or veto");
assert!(changed);
assert!(layer.prim(Path::new("/World").unwrap()).unwrap().is_some());
}
#[test]
fn edit_rolls_back_on_error() {
use crate::sdf::{PrimSpec, Specifier};
let mut layer = Layer::new_anonymous("test.usda");
let result: Result<bool, EditError> = layer.edit(|l| {
PrimSpec::new(l.data_mut(), "/A", Specifier::Def, "Xform")?;
Err(AuthoringError::InvalidPath {
path: Path::abs_root(),
reason: "test",
})
});
assert!(result.is_err());
assert!(
layer.prim(Path::new("/A").unwrap()).unwrap().is_none(),
"the staged edit rolled back"
);
assert!(!layer.is_dirty());
}
#[test]
fn edit_recovers_on_panic() {
use crate::sdf::{PrimSpec, Specifier};
use std::panic::{AssertUnwindSafe, catch_unwind};
let mut layer = Layer::new_anonymous("test.usda");
let panicked = catch_unwind(AssertUnwindSafe(|| {
let _: Result<bool, EditError> = layer.edit(|l| {
PrimSpec::new(l.data_mut(), "/A", Specifier::Def, "Xform").expect("authored");
panic!("boom");
});
}));
assert!(panicked.is_err(), "the panic propagates");
assert!(
layer.prim(Path::new("/A").unwrap()).unwrap().is_none(),
"the staged edit rolled back"
);
assert!(!layer.is_dirty(), "no staged edits or recorded changes remain");
let changed = layer
.edit(|l| {
PrimSpec::new(l.data_mut(), "/B", Specifier::Def, "Xform")?;
Ok(())
})
.expect("no error");
assert!(changed);
assert!(layer.prim(Path::new("/B").unwrap()).unwrap().is_some());
}
#[test]
fn group_commit_atomic_on_panic() {
use crate::sdf::{PrimSpec, Specifier};
use std::panic::{AssertUnwindSafe, catch_unwind};
let mut a = Layer::new_anonymous("a.usda");
let mut b = Layer::new_anonymous("b.usda");
a.add_sink(|_: &str, _: &ChangeList| panic!("boom"));
let panicked = catch_unwind(AssertUnwindSafe(|| {
let _ = edit_layers(&mut [&mut a, &mut b], |edits| {
PrimSpec::new(edits[0].data_mut(), "/A", Specifier::Def, "Xform").expect("authored a");
PrimSpec::new(edits[1].data_mut(), "/B", Specifier::Def, "Xform").expect("authored b");
Ok::<(), EditError>(())
});
}));
assert!(panicked.is_err(), "the after_commit panic propagates");
assert!(
a.prim(Path::new("/A").unwrap()).unwrap().is_some(),
"layer a committed despite the panic"
);
assert!(
b.prim(Path::new("/B").unwrap()).unwrap().is_some(),
"layer b committed despite the panic"
);
}
#[test]
fn discard_changes_keeps_content() {
use crate::sdf::{PrimSpec, Specifier};
let mut layer = Layer::new_anonymous("test.usda");
edit_layer(&mut layer, |e| {
PrimSpec::new(e.data_mut(), "/World", Specifier::Def, "Xform").unwrap();
});
layer.discard_changes();
assert!(!layer.is_dirty());
assert!(
layer.prim(Path::new("/World").unwrap()).unwrap().is_some(),
"the authored content survives"
);
}
#[test]
fn default_prim_path_forms() {
for (token, want) in [
("/foo", "/foo"),
("/foo/bar", "/foo/bar"),
("foo", "/foo"),
("foo/bar", "/foo/bar"),
] {
assert_eq!(default_prim_path(token).as_ref().map(Path::as_str), Some(want));
}
for token in ["/foo.prop", "/", "//", "", "..", "."] {
assert_eq!(default_prim_path(token), None, "{token} names no prim");
}
}
#[test]
fn set_default_prim_forms() {
let mut layer = Layer::new_anonymous("anon.usda");
for name in ["World", "World/Char", "/World/Char"] {
edit_layer(&mut layer, |e| {
e.set_default_prim(name).expect("names a prim");
});
assert_eq!(layer.pseudo_root().unwrap().default_prim().as_deref(), Some(name));
}
edit_layer(&mut layer, |e| {
assert!(e.set_default_prim("World.attr").is_err(), "a property names no prim");
assert!(e.set_default_prim("/").is_err(), "the pseudo-root names no prim");
});
}
#[test]
fn metadata_on_rootless_backend() {
let root = Path::abs_root();
let default_prim = FieldKey::DefaultPrim.as_str();
let mut layer = Layer::new("rootless.usda", Box::new(Data::new()));
edit_layer(&mut layer, |e| {
e.set_default_prim("World").unwrap();
});
assert!(layer.data().has_field(&root, default_prim));
edit_layer(&mut layer, |e| {
e.clear_default_prim().unwrap();
});
assert!(!layer.data().has_field(&root, default_prim));
}
#[derive(Default)]
struct Recorder {
before: std::cell::Cell<u32>,
after: std::cell::Cell<u32>,
veto: bool,
}
impl LayerSink for std::rc::Rc<Recorder> {
fn before_commit(&self, _change: &PendingLayerChange<'_>) -> Result<(), sink::Error> {
self.before.set(self.before.get() + 1);
if self.veto {
return Err(sink::Error::new("vetoed"));
}
Ok(())
}
fn after_commit(&self, _layer: &str, _changes: &ChangeList) {
self.after.set(self.after.get() + 1);
}
}
#[test]
fn layer_sink_fires() {
use crate::sdf::{PrimSpec, Specifier};
let mut layer = Layer::new_anonymous("test.usda");
let rec = std::rc::Rc::new(Recorder::default());
layer.add_sink(rec.clone());
layer
.edit(|l| {
PrimSpec::new(l.data_mut(), "/World", Specifier::Def, "Xform")?;
Ok(())
})
.expect("sink accepts");
assert_eq!((rec.before.get(), rec.after.get()), (1, 1));
assert!(layer.prim(Path::new("/World").unwrap()).unwrap().is_some());
}
#[test]
fn layer_sink_veto_rolls_back() {
use crate::sdf::{PrimSpec, Specifier};
let mut layer = Layer::new_anonymous("test.usda");
let rec = std::rc::Rc::new(Recorder {
veto: true,
..Default::default()
});
layer.add_sink(rec.clone());
let result = layer.edit(|l| {
PrimSpec::new(l.data_mut(), "/World", Specifier::Def, "Xform")?;
Ok(())
});
assert!(matches!(result, Err(EditError::Rejected(_))), "the sink vetoes");
assert_eq!((rec.before.get(), rec.after.get()), (1, 0), "after_commit never fired");
assert!(
layer.prim(Path::new("/World").unwrap()).unwrap().is_none(),
"the edit rolled back"
);
assert!(!layer.is_dirty());
}
#[test]
fn layer_sink_removed() {
use crate::sdf::{PrimSpec, Specifier};
let mut layer = Layer::new_anonymous("test.usda");
let rec = std::rc::Rc::new(Recorder::default());
let id = layer.add_sink(rec.clone());
layer.remove_sink(id);
layer
.edit(|l| {
PrimSpec::new(l.data_mut(), "/World", Specifier::Def, "Xform")?;
Ok(())
})
.expect("no sink");
assert_eq!((rec.before.get(), rec.after.get()), (0, 0));
}
#[test]
fn export_dispatches_on_extension() -> Result<()> {
use crate::sdf::{self, SpecType};
let mut data = sdf::Data::new();
let root = sdf::Path::abs_root();
let ps = data.create_spec(root, SpecType::PseudoRoot);
ps.add("primChildren", sdf::Value::TokenVec(vec!["Foo".into()]));
let foo = sdf::path("/Foo")?;
let sp = data.create_spec(foo, SpecType::Prim);
sp.add("specifier", sdf::Value::Specifier(sdf::Specifier::Def));
sp.add("typeName", sdf::Value::Token("Xform".into()));
let layer = sdf::Layer::new("test://layer", Box::new(data));
let dir = tempfile::tempdir()?;
let usda_path = dir.path().join("layer-save.usda");
let usdc_path = dir.path().join("layer-save.usdc");
let usdz_path = dir.path().join("layer-save.usdz");
layer.export(usda_path.to_str().unwrap())?;
layer.export(usdc_path.to_str().unwrap())?;
layer.export(usdz_path.to_str().unwrap())?;
assert!(std::fs::metadata(&usda_path)?.len() > 0);
assert!(std::fs::metadata(&usdc_path)?.len() > 0);
assert!(std::fs::metadata(&usdz_path)?.len() > 0);
let archive = crate::usdz::Archive::open(&usdz_path)?;
let name = archive.first_layer_name().expect("usdz has a layer");
assert!(name.ends_with(".usdc"));
Ok(())
}
#[test]
fn export_usd_writes_binary() -> Result<()> {
use crate::sdf::{self, SpecType};
let mut data = sdf::Data::new();
let root = sdf::Path::abs_root();
let ps = data.create_spec(root, SpecType::PseudoRoot);
ps.add("primChildren", sdf::Value::TokenVec(vec!["Bar".into()]));
let bar = sdf::path("/Bar")?;
let sp = data.create_spec(bar.clone(), SpecType::Prim);
sp.add("specifier", sdf::Value::Specifier(sdf::Specifier::Def));
sp.add("typeName", sdf::Value::Token("Cube".into()));
let layer = sdf::Layer::new("test://layer-usd", Box::new(data));
let dir = tempfile::tempdir()?;
let path = dir.path().join("layer-save.usd");
layer.export(path.to_str().unwrap())?;
let bytes = std::fs::read(&path)?;
assert!(
bytes.starts_with(crate::usdc::MAGIC),
"writer should emit binary for .usd, got magic {:?}",
&bytes[..crate::usdc::MAGIC.len().min(bytes.len())],
);
let round = sdf::LayerRegistry::default()
.open(path.to_str().unwrap())?
.expect("the layer opens")
.data;
assert_eq!(round.spec_type(&bar), Some(SpecType::Prim));
assert_eq!(
round.get_field(&bar, "typeName").unwrap().into_owned(),
sdf::Value::Token("Cube".into())
);
Ok(())
}
#[test]
fn export_rejects_unknown_ext() {
use crate::sdf::{self, SpecType};
let mut data = sdf::Data::new();
data.create_spec(sdf::Path::abs_root(), SpecType::PseudoRoot);
let layer = sdf::Layer::new("test://layer", Box::new(data));
let err = layer.export("/tmp/openusd-bad.xyz").unwrap_err();
assert!(err.to_string().contains("unsupported"));
}
#[test]
fn save_writes_identifier() -> Result<()> {
use crate::sdf::{self, SpecType};
let mut data = sdf::Data::new();
data.create_spec(sdf::Path::abs_root(), SpecType::PseudoRoot);
let dir = tempfile::tempdir()?;
let path = dir.path().join("by-identifier.usda");
let mut layer = sdf::Layer::new(path.to_str().unwrap(), Box::new(data));
layer.save()?;
let bytes = std::fs::read(&path)?;
assert!(
bytes.starts_with(b"#usda"),
"save() should write text to a .usda identifier"
);
Ok(())
}
#[test]
fn save_rejects_anonymous() {
let mut layer = sdf::Layer::new_anonymous("scratch");
let err = layer.save().unwrap_err();
assert!(err.to_string().contains("anonymous"));
}
#[test]
fn save_rejects_non_file_identifier() {
use crate::sdf::{self, SpecType};
let mut data = sdf::Data::new();
data.create_spec(sdf::Path::abs_root(), SpecType::PseudoRoot);
let mut layer = sdf::Layer::new("scheme://host/x.usda", Box::new(data));
let err = layer.save().unwrap_err();
assert!(err.to_string().contains("absolute file path"));
}
#[test]
fn save_rejects_usdz_in_place() {
use crate::sdf::{self, SpecType};
let mut data = sdf::Data::new();
data.create_spec(sdf::Path::abs_root(), SpecType::PseudoRoot);
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("pkg.usdz");
let mut layer = sdf::Layer::new(path.to_str().unwrap(), Box::new(data));
let err = layer.save().unwrap_err();
assert!(err.to_string().contains("not editable"));
}
#[test]
fn export_to_string_is_usda() -> Result<()> {
use crate::sdf::{self, SpecType};
let mut data = sdf::Data::new();
let ps = data.create_spec(sdf::Path::abs_root(), SpecType::PseudoRoot);
ps.add("primChildren", sdf::Value::TokenVec(vec!["Foo".into()]));
let sp = data.create_spec(sdf::path("/Foo")?, SpecType::Prim);
sp.add("specifier", sdf::Value::Specifier(sdf::Specifier::Def));
let layer = sdf::Layer::new("scratch.usdc", Box::new(data));
let text = layer.export_to_string()?;
assert!(text.starts_with("#usda"), "export_to_string must emit usda text");
assert!(text.contains("\"Foo\""), "export_to_string must include the prim");
Ok(())
}
#[test]
fn create_prim_basic() -> Result<()> {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
edit_layer(&mut layer, |e| {
sdf::PrimSpec::new(e.data_mut(), "/World", sdf::Specifier::Def, "Xform")
.unwrap()
.set_kind("group");
});
let world = layer.prim("/World")?.expect("prim authored");
assert_eq!(world.type_name().as_deref(), Some("Xform"));
assert_eq!(world.specifier(), Some(sdf::Specifier::Def));
assert_eq!(world.kind().as_deref(), Some("group"));
Ok(())
}
#[test]
fn auto_ancestor_chain() -> Result<()> {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
edit_layer(&mut layer, |e| {
sdf::PrimSpec::new(e.data_mut(), "/A/B/C", sdf::Specifier::Def, "Mesh").unwrap();
});
assert_eq!(
layer.prim("/A/B/C")?.and_then(|p| p.specifier()),
Some(sdf::Specifier::Def)
);
assert_eq!(
layer.prim("/A/B")?.and_then(|p| p.specifier()),
Some(sdf::Specifier::Over)
);
assert_eq!(
layer.prim("/A")?.and_then(|p| p.specifier()),
Some(sdf::Specifier::Over)
);
Ok(())
}
#[test]
fn prim_children() -> Result<()> {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
edit_layer(&mut layer, |e| {
sdf::PrimSpec::new(e.data_mut(), "/World", sdf::Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/World/Mesh", sdf::Specifier::Def, "Mesh").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/World/Cube", sdf::Specifier::Def, "Cube").unwrap();
});
let root = layer.pseudo_root().expect("pseudo-root present");
let root_children = root.prim_children().unwrap();
let root_children: Vec<&str> = root_children.iter().map(|t| t.as_str()).collect();
assert_eq!(root_children, ["World"]);
let world = layer.prim("/World")?.expect("prim");
let world_children = world.prim_children().unwrap();
let world_children: Vec<&str> = world_children.iter().map(|t| t.as_str()).collect();
assert_eq!(world_children, ["Mesh", "Cube"]);
Ok(())
}
#[test]
fn property_children() -> Result<()> {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
edit_layer(&mut layer, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Mesh", sdf::Specifier::Def, "Mesh").unwrap();
sdf::AttributeSpec::new(
e.data_mut(),
"/Mesh.points",
"point3f[]",
sdf::Variability::Varying,
false,
)
.unwrap();
sdf::AttributeSpec::new(
e.data_mut(),
"/Mesh.normals",
"normal3f[]",
sdf::Variability::Varying,
false,
)
.unwrap();
sdf::RelationshipSpec::new(e.data_mut(), "/Mesh.material:binding", sdf::Variability::Varying, false)
.unwrap();
});
let mesh = layer.prim("/Mesh")?.expect("prim");
let props = mesh.property_children().unwrap();
let props: Vec<&str> = props.iter().map(|t| t.as_str()).collect();
assert_eq!(props, ["points", "normals", "material:binding"]);
Ok(())
}
#[test]
fn relationship_variability() -> Result<()> {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
edit_layer(&mut layer, |e| {
sdf::RelationshipSpec::new(e.data_mut(), "/Mesh.material:binding", sdf::Variability::Uniform, false)
.unwrap();
let error =
sdf::RelationshipSpec::new(e.data_mut(), "/Mesh.material:binding", sdf::Variability::Varying, false)
.expect_err("a spec already at the path");
assert!(matches!(error, sdf::AuthoringError::InvalidPath { .. }));
sdf::RelationshipSpec::new(e.data_mut(), "/Mesh.varying", sdf::Variability::Varying, false).unwrap();
});
let rel = layer.relationship("/Mesh.material:binding")?.expect("relationship");
assert_eq!(rel.variability(), sdf::Variability::Uniform);
let rel = layer.relationship("/Mesh.varying")?.expect("relationship");
assert_eq!(rel.variability(), sdf::Variability::Varying);
assert!(rel.field(sdf::FieldKey::Variability.as_str()).ok().flatten().is_none());
Ok(())
}
#[test]
fn bad_prim_children_errors() {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
edit_layer(&mut layer, |e| {
e.data_mut().set_field(
&sdf::Path::abs_root(),
sdf::ChildrenKey::PrimChildren.as_str(),
sdf::Value::String("bad".into()),
);
});
let err = layer
.edit(|e| {
sdf::PrimSpec::new(e.data_mut(), "/A", sdf::Specifier::Def, "Xform")?;
Ok(())
})
.unwrap_err();
assert!(matches!(
err,
sdf::EditError::Author(sdf::AuthoringError::InvalidPath { .. })
));
let value = layer
.data()
.get_field(&sdf::Path::abs_root(), sdf::ChildrenKey::PrimChildren.as_str())
.unwrap()
.into_owned();
assert!(matches!(value, sdf::Value::String(value) if value == "bad"));
}
#[test]
fn bad_property_children_errors() -> Result<()> {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
edit_layer(&mut layer, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Mesh", sdf::Specifier::Def, "Mesh").unwrap();
e.data_mut().set_field(
&sdf::path("/Mesh").unwrap(),
sdf::ChildrenKey::PropertyChildren.as_str(),
sdf::Value::String("bad".into()),
);
});
let err = layer
.edit(|e| {
sdf::RelationshipSpec::new(e.data_mut(), "/Mesh.material:binding", sdf::Variability::Varying, false)?;
Ok(())
})
.unwrap_err();
assert!(matches!(
err,
sdf::EditError::Author(sdf::AuthoringError::InvalidPath { .. })
));
assert!(!layer.data().has_spec(&sdf::path("/Mesh.material:binding").unwrap()));
let value = layer
.data()
.get_field(
&sdf::path("/Mesh").unwrap(),
sdf::ChildrenKey::PropertyChildren.as_str(),
)
.unwrap()
.into_owned();
assert!(matches!(value, sdf::Value::String(value) if value == "bad"));
Ok(())
}
#[test]
fn attr_samples() -> Result<()> {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
edit_layer(&mut layer, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Sphere", sdf::Specifier::Def, "Sphere").unwrap();
let mut radius = sdf::AttributeSpec::new(
e.data_mut(),
"/Sphere.radius",
"double",
sdf::Variability::Varying,
false,
)
.unwrap();
radius
.set_default(sdf::Value::Double(2.5))
.expect("value fits the declared type");
radius
.set_time_sample(0.0, sdf::Value::Double(1.0))
.expect("value fits the declared type");
radius
.set_time_sample(10.0, sdf::Value::Double(3.0))
.expect("value fits the declared type");
radius
.set_time_sample(5.0, sdf::Value::Double(2.0))
.expect("value fits the declared type");
});
let read = layer.attribute("/Sphere.radius")?.expect("attr");
assert_eq!(read.type_name(), Some(sdf::ValueTypeName::DOUBLE));
assert_eq!(read.default(), Some(sdf::Value::Double(2.5)));
let samples = read.time_samples().expect("samples authored");
let times: Vec<f64> = samples.iter().map(|(t, _)| *t).collect();
assert_eq!(times, vec![0.0, 5.0, 10.0]);
Ok(())
}
#[test]
fn wrong_ancestor_type() {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
edit_layer(&mut layer, |e| {
e.data_mut()
.create_spec(sdf::path("/A").unwrap(), sdf::SpecType::Attribute);
});
let err = layer
.edit(|e| {
sdf::PrimSpec::new(e.data_mut(), "/A/B", sdf::Specifier::Def, "Xform")?;
Ok(())
})
.unwrap_err();
assert!(matches!(
err,
sdf::EditError::Author(sdf::AuthoringError::InvalidPath { .. })
));
let err = layer
.edit(|e| {
sdf::AttributeSpec::new(e.data_mut(), "/A.x", "double", sdf::Variability::Varying, false)?;
Ok(())
})
.unwrap_err();
assert!(matches!(
err,
sdf::EditError::Author(sdf::AuthoringError::InvalidPath { .. })
));
}
#[test]
fn failed_prim_chain_creation_is_atomic() {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
let bad_child = sdf::path("/A/B").unwrap();
edit_layer(&mut layer, |e| {
e.data_mut().create_spec(bad_child, sdf::SpecType::Attribute);
});
let err = layer
.edit(|e| {
sdf::PrimSpec::new(e.data_mut(), "/A/B", sdf::Specifier::Def, "Xform")?;
Ok(())
})
.unwrap_err();
assert!(matches!(
err,
sdf::EditError::Author(sdf::AuthoringError::InvalidPath { .. })
));
assert!(!layer.data().has_spec(&sdf::path("/A").unwrap()));
assert!(
layer
.data()
.try_field(&sdf::Path::abs_root(), sdf::ChildrenKey::PrimChildren.as_str())
.unwrap()
.is_none()
);
}
#[test]
fn nan_time_sample() -> Result<()> {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
edit_layer(&mut layer, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Sphere", sdf::Specifier::Def, "Sphere").unwrap();
let mut r = sdf::AttributeSpec::new(
e.data_mut(),
"/Sphere.radius",
"double",
sdf::Variability::Varying,
false,
)
.unwrap();
r.set_time_sample(1.0, sdf::Value::Double(1.0))
.expect("value fits the declared type");
r.set_time_sample(f64::NAN, sdf::Value::Double(99.0))
.expect("value fits the declared type");
r.set_time_sample(2.0, sdf::Value::Double(2.0))
.expect("value fits the declared type");
});
let samples = layer
.attribute("/Sphere.radius")?
.unwrap()
.time_samples()
.unwrap()
.clone();
let finite: Vec<f64> = samples.iter().map(|(t, _)| *t).filter(|t| t.is_finite()).collect();
assert_eq!(finite, vec![1.0, 2.0]);
let mut erased = false;
edit_layer(&mut layer, |e| {
erased = e
.attribute_mut("/Sphere.radius")
.unwrap()
.unwrap()
.erase_time_sample(f64::NAN)
.expect("samples readable");
});
assert!(erased);
let times: Vec<f64> = layer
.attribute("/Sphere.radius")?
.unwrap()
.time_samples()
.unwrap()
.iter()
.map(|(t, _)| *t)
.collect();
assert_eq!(times, vec![1.0, 2.0]);
Ok(())
}
#[test]
fn override_prim() -> Result<()> {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
edit_layer(&mut layer, |e| {
sdf::PrimSpec::over(e.data_mut(), "/A/B").unwrap();
});
assert_eq!(
layer.prim("/A")?.and_then(|p| p.specifier()),
Some(sdf::Specifier::Over)
);
assert_eq!(
layer.prim("/A/B")?.and_then(|p| p.specifier()),
Some(sdf::Specifier::Over)
);
edit_layer(&mut layer, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Defined", sdf::Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::over(e.data_mut(), "/Defined").unwrap();
});
assert_eq!(
layer.prim("/Defined")?.and_then(|p| p.specifier()),
Some(sdf::Specifier::Def)
);
Ok(())
}
#[test]
fn pseudo_root_metadata() -> Result<()> {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
edit_layer(&mut layer, |e| {
let mut root = e.pseudo_root_mut().expect("writable");
root.set_default_prim("World");
root.set_documentation("auto-generated");
root.set_start_time_code(1.0);
root.set_end_time_code(24.0);
root.add_sublayer("./over.usda");
root.add_sublayer("./over.usda");
});
let root = layer.pseudo_root().expect("present");
assert_eq!(root.default_prim().as_deref(), Some("World"));
assert_eq!(root.documentation().as_deref(), Some("auto-generated"));
assert_eq!(root.start_time_code(), Some(1.0));
assert_eq!(root.end_time_code(), Some(24.0));
assert_eq!(
root.sublayers(),
Some(vec!["./over.usda".to_string(), "./over.usda".to_string()])
);
Ok(())
}
#[test]
fn invalid_paths() {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
fn invalid(
layer: &mut sdf::Layer,
f: impl FnOnce(&mut sdf::LayerEdit<'_>) -> Result<(), sdf::AuthoringError>,
) -> bool {
matches!(
layer.edit(f).unwrap_err(),
sdf::EditError::Author(sdf::AuthoringError::InvalidPath { .. })
)
}
assert!(invalid(&mut layer, |e| {
sdf::PrimSpec::new(e.data_mut(), "/", sdf::Specifier::Def, "Xform")?;
Ok(())
}));
assert!(invalid(&mut layer, |e| {
sdf::PrimSpec::new(e.data_mut(), "/A.foo", sdf::Specifier::Def, "Xform")?;
Ok(())
}));
assert!(invalid(&mut layer, |e| {
sdf::AttributeSpec::new(e.data_mut(), "/A", "double", sdf::Variability::Varying, false)?;
Ok(())
}));
assert!(invalid(&mut layer, |e| {
sdf::AttributeSpec::new(e.data_mut(), "A.foo", "double", sdf::Variability::Varying, false)?;
Ok(())
}));
assert!(invalid(&mut layer, |e| {
sdf::RelationshipSpec::new(e.data_mut(), "A.foo", sdf::Variability::Varying, false)?;
Ok(())
}));
assert!(matches!(
layer
.edit(|e| {
sdf::AttributeSpec::new(e.data_mut(), "/.foo", "double", sdf::Variability::Varying, false)?;
Ok(())
})
.unwrap_err(),
sdf::EditError::Author(sdf::AuthoringError::Parse(_))
));
assert!(invalid(&mut layer, |e| {
sdf::AttributeSpec::new(
e.data_mut(),
"/A.rel[/Target].attr",
"double",
sdf::Variability::Varying,
false,
)?;
Ok(())
}));
assert!(invalid(&mut layer, |e| {
sdf::PrimSpec::new(e.data_mut(), "/A{x=}child", sdf::Specifier::Def, "Xform")?;
Ok(())
}));
}
#[test]
fn variant_authoring() -> Result<()> {
let mut layer = sdf::Layer::new_anonymous("variants.usda");
edit_layer(&mut layer, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Prim", sdf::Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Prim{set=sel}child", sdf::Specifier::Def, "Scope").unwrap();
});
assert_eq!(layer.data().spec_type(&sdf::path("/Prim")?), Some(sdf::SpecType::Prim));
assert_eq!(
layer.data().spec_type(&sdf::path("/Prim{set=}")?),
Some(sdf::SpecType::VariantSet)
);
assert_eq!(
layer.data().spec_type(&sdf::path("/Prim{set=sel}")?),
Some(sdf::SpecType::Variant)
);
assert_eq!(
layer.data().spec_type(&sdf::path("/Prim{set=sel}child")?),
Some(sdf::SpecType::Prim)
);
let token_vec = |path: &str, key: sdf::ChildrenKey| -> Result<Vec<String>> {
match layer.data().get_field(&sdf::path(path)?, key.as_str())?.into_owned() {
sdf::Value::TokenVec(v) => Ok(v.into_iter().map(Into::into).collect()),
other => panic!("expected TokenVec at {path}, got {other:?}"),
}
};
assert_eq!(token_vec("/Prim", sdf::ChildrenKey::VariantSetChildren)?, vec!["set"]);
assert_eq!(
token_vec("/Prim{set=}", sdf::ChildrenKey::VariantChildren)?,
vec!["sel"]
);
assert_eq!(
token_vec("/Prim{set=sel}", sdf::ChildrenKey::PrimChildren)?,
vec!["child"]
);
Ok(())
}
#[test]
fn prim_authoring_rejects_variant_leaf() -> Result<()> {
let mut layer = sdf::Layer::new_anonymous("variants.usda");
edit_layer(&mut layer, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Prim", sdf::Specifier::Def, "Xform").unwrap();
});
assert!(matches!(
layer
.edit(|e| {
sdf::PrimSpec::new(e.data_mut(), "/Prim{set=sel}", sdf::Specifier::Def, "Xform")?;
Ok(())
})
.unwrap_err(),
sdf::EditError::Author(sdf::AuthoringError::InvalidPath { .. })
));
assert!(matches!(
layer
.edit(|e| {
sdf::PrimSpec::over(e.data_mut(), "/Prim{set=sel}")?;
Ok(())
})
.unwrap_err(),
sdf::EditError::Author(sdf::AuthoringError::InvalidPath { .. })
));
assert_eq!(layer.data().spec_type(&sdf::path("/Prim{set=sel}")?), None);
Ok(())
}
#[test]
fn usda_roundtrip() -> Result<()> {
let mut layer = sdf::Layer::new_anonymous("scene.usda");
let material = sdf::path("/World/Material")?;
edit_layer(&mut layer, |e| {
e.pseudo_root_mut().unwrap().set_default_prim("World");
sdf::PrimSpec::new(e.data_mut(), "/World", sdf::Specifier::Def, "Xform").unwrap();
let mut sphere = sdf::PrimSpec::new(e.data_mut(), "/World/Sphere", sdf::Specifier::Def, "Sphere").unwrap();
sphere.set_kind("component");
let mut radius = sdf::AttributeSpec::new(
e.data_mut(),
"/World/Sphere.radius",
"double",
sdf::Variability::Varying,
false,
)
.unwrap();
radius
.set_default(sdf::Value::Double(1.5))
.expect("value fits the declared type");
sdf::PrimSpec::new(e.data_mut(), &material, sdf::Specifier::Def, "Material").unwrap();
let mut binding = sdf::RelationshipSpec::new(
e.data_mut(),
"/World/Sphere.material:binding",
sdf::Variability::Varying,
false,
)
.unwrap();
binding.add_target(material.clone()).unwrap();
let mut surface = sdf::AttributeSpec::new(
e.data_mut(),
"/World/Sphere.inputs:surface",
"token",
sdf::Variability::Varying,
false,
)
.unwrap();
surface
.set_connection_paths([sdf::path("/World/Material.outputs:surface").unwrap()])
.unwrap();
});
let tmp = std::env::temp_dir().join("openusd_authoring_roundtrip.usda");
layer.export(tmp.to_str().unwrap())?;
let parsed = usda::read_file(&tmp)?;
assert_eq!(parsed.spec_type(&sdf::path("/World")?), Some(sdf::SpecType::Prim));
assert_eq!(
parsed.spec_type(&sdf::path("/World/Sphere")?),
Some(sdf::SpecType::Prim)
);
assert_eq!(
parsed.spec_type(&sdf::path("/World/Sphere.radius")?),
Some(sdf::SpecType::Attribute)
);
assert_eq!(
parsed
.get_field(&sdf::Path::abs_root(), sdf::FieldKey::DefaultPrim.as_str())?
.into_owned(),
sdf::Value::Token("World".into())
);
let targets = parsed
.get_field(
&sdf::path("/World/Sphere.material:binding")?,
sdf::FieldKey::TargetPaths.as_str(),
)?
.into_owned()
.try_as_path_list_op()
.expect("relationship targets as PathListOp");
assert!(targets.explicit);
assert_eq!(targets.explicit_items, vec![material]);
let connections = parsed
.get_field(
&sdf::path("/World/Sphere.inputs:surface")?,
sdf::FieldKey::ConnectionPaths.as_str(),
)?
.into_owned()
.try_as_path_list_op()
.expect("connection paths as PathListOp");
assert!(connections.explicit);
assert_eq!(
connections.explicit_items,
vec![sdf::path("/World/Material.outputs:surface")?]
);
let _ = std::fs::remove_file(&tmp);
Ok(())
}
fn opened_through(layer: &mut Layer, open: impl Fn() -> io::Result<Box<dyn ar::Asset>> + 'static) {
layer.resolver = Some(Rc::new(ar::tests::TestResolver(open)));
}
fn edited_fixture(dir: &FsPath) -> Result<(PathBuf, Layer)> {
let path = crate_fixture(dir)?;
let mut layer = Layer::open(path.to_string_lossy())?;
set_component(&mut layer);
Ok((path, layer))
}
fn shared_asset(bytes: Vec<u8>, mapping: bool, sources: &RefCell<Vec<ar::tests::AdviceLog>>) -> Box<dyn ar::Asset> {
let (buffer, log) = ar::tests::recording_source(bytes, mapping);
sources.borrow_mut().push(log);
Box::new(io::Cursor::new(ar::AssetBuffer::Shared(buffer)))
}
fn live_sources(sources: &RefCell<Vec<ar::tests::AdviceLog>>) -> usize {
sources.borrow().iter().filter(|log| Arc::strong_count(log) > 1).count()
}
#[test]
fn save_remaps() -> Result<()> {
let dir = tempfile::tempdir()?;
let (path, mut layer) = edited_fixture(dir.path())?;
let sources = Rc::new(RefCell::new(Vec::new()));
opened_through(&mut layer, {
let (path, sources) = (path.clone(), Rc::clone(&sources));
move || Ok(shared_asset(fs::read(&path)?, true, &sources))
});
layer.save()?;
assert_eq!(sources.borrow().len(), 1, "the saved file is opened once");
assert_eq!(live_sources(&sources), 1, "the layer holds the mapping");
assert_eq!(world_kind(&layer), Some(tf::Token::from("component")));
let size = layer.data().get_field(&Path::new("/World.size")?, "default")?;
assert_eq!(size.into_owned(), Value::from(vec![1.0_f32; 64]));
Ok(())
}
#[test]
fn save_refuses_stale() -> Result<()> {
let dir = tempfile::tempdir()?;
let (path, mut layer) = edited_fixture(dir.path())?;
let fired = Rc::new(Cell::new(0));
layer.add_sink({
let fired = Rc::clone(&fired);
move |_: &str, _: &ChangeList| fired.set(fired.get() + 1)
});
let before = fs::read(&path)?;
let sources = Rc::new(RefCell::new(Vec::new()));
opened_through(&mut layer, {
let sources = Rc::clone(&sources);
move || Ok(shared_asset(before.clone(), true, &sources))
});
layer.save()?;
assert_eq!(sources.borrow().len(), 1);
assert_eq!(live_sources(&sources), 0, "the stale mapping is let go");
assert_eq!(world_kind(&layer), Some(tf::Token::from("component")));
assert_eq!(fired.get(), 0, "a save is not a layer change");
assert_eq!(
world_kind(&Layer::open(path.to_string_lossy())?),
Some(tf::Token::from("component"))
);
Ok(())
}
#[test]
fn save_skips_heap() -> Result<()> {
let dir = tempfile::tempdir()?;
let (path, mut layer) = edited_fixture(dir.path())?;
let sources = Rc::new(RefCell::new(Vec::new()));
opened_through(&mut layer, {
let sources = Rc::clone(&sources);
move || Ok(shared_asset(fs::read(&path)?, false, &sources))
});
layer.save()?;
assert_eq!(sources.borrow().len(), 1);
assert_eq!(live_sources(&sources), 0);
assert_eq!(world_kind(&layer), Some(tf::Token::from("component")));
Ok(())
}
#[test]
fn save_reads_nothing() -> Result<()> {
struct Counting(io::Cursor<Vec<u8>>, Arc<AtomicUsize>);
impl io::Read for Counting {
fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
self.1.fetch_add(1, Ordering::Relaxed);
self.0.read(buffer)
}
}
impl io::Seek for Counting {
fn seek(&mut self, position: io::SeekFrom) -> io::Result<u64> {
self.0.seek(position)
}
}
impl ar::Asset for Counting {
fn size(&self) -> io::Result<u64> {
Ok(self.0.get_ref().len() as u64)
}
}
let dir = tempfile::tempdir()?;
let (path, mut layer) = edited_fixture(dir.path())?;
let (opens, reads) = (Rc::new(Cell::new(0)), Arc::new(AtomicUsize::new(0)));
opened_through(&mut layer, {
let (opens, reads) = (Rc::clone(&opens), Arc::clone(&reads));
move || {
opens.set(opens.get() + 1);
Ok(Box::new(Counting(
io::Cursor::new(fs::read(&path)?),
Arc::clone(&reads),
)))
}
});
layer.save()?;
assert_eq!(opens.get(), 1);
assert_eq!(reads.load(Ordering::Relaxed), 0);
assert_eq!(world_kind(&layer), Some(tf::Token::from("component")));
Ok(())
}
#[test]
fn save_survives_reopen() -> Result<()> {
let dir = tempfile::tempdir()?;
let (path, mut layer) = edited_fixture(dir.path())?;
opened_through(&mut layer, || Err(io::Error::from(io::ErrorKind::PermissionDenied)));
layer.save()?;
let component = Some(tf::Token::from("component"));
assert_eq!(world_kind(&layer), component);
assert_eq!(world_kind(&Layer::open(path.to_string_lossy())?), component);
Ok(())
}
#[test]
fn save_replace_failure() -> Result<()> {
let dir = tempfile::tempdir()?;
let (path, mut layer) = edited_fixture(dir.path())?;
let opens = Rc::new(Cell::new(0));
opened_through(&mut layer, {
let opens = Rc::clone(&opens);
move || {
opens.set(opens.get() + 1);
Err(io::Error::from(io::ErrorKind::NotFound))
}
});
fs::remove_file(&path)?;
fs::create_dir(&path)?;
let error = layer.save().expect_err("the replacement fails");
assert!(matches!(error, ExportError::Replace { .. }), "{error:?}");
assert_eq!(opens.get(), 0);
assert_eq!(world_kind(&layer), Some(tf::Token::from("component")));
Ok(())
}
#[cfg(feature = "mmap")]
#[test]
#[allow(unsafe_code)]
fn save_mapped() -> Result<()> {
let dir = tempfile::tempdir()?;
let path = crate_fixture(dir.path())?;
let resolver = unsafe { ar::DefaultResolver::new().map_files() };
let registry = sdf::LayerRegistry::new(Box::new(resolver));
let identifier = registry.create_identifier(&path.to_string_lossy(), None);
let mut editing = registry.open_layer(&identifier)?.expect("the layer resolves");
let other = registry.open_layer(&identifier)?.expect("the layer resolves");
set_component(&mut editing);
editing.save()?;
let component = Some(tf::Token::from("component"));
assert_eq!(world_kind(&editing), component);
assert_eq!(world_kind(&other), None, "the other view reads the file it mapped");
assert_eq!(world_kind(&Layer::open(path.to_string_lossy())?), component);
let size = editing.data().get_field(&Path::new("/World.size")?, "default")?;
assert_eq!(size.into_owned(), Value::from(vec![1.0_f32; 64]));
#[cfg(windows)]
{
let write = || fs::OpenOptions::new().write(true).open(&path);
assert!(write().is_err(), "the saved layer maps its file");
drop(editing);
assert!(write().is_ok(), "the mapping went with the layer");
}
Ok(())
}
}