use std::collections::{HashMap, HashSet};
use std::io::{Cursor, Read, Seek, Write};
use std::path::Path;
use super::discover::{Content, Discover, Discovery, Placement, Source, Use, discover};
use super::walk::{Visit, apply_edits, visit_asset_paths};
use crate::sdf::{self, AbstractData};
use crate::usd::Stage;
use crate::{Error, Result, ar, pcp, tf, usdz};
pub fn create_new_usdz_package(
stage: &Stage,
asset_path: &str,
usdz_file_path: impl AsRef<Path>,
first_layer_name: Option<&str>,
) -> Result<Vec<String>> {
let graph = stage.layers();
let policy = Discover {
inactive: true,
open_packages: false,
};
let discovery = discover(&graph, asset_path, policy)?;
let mut skipped = Vec::new();
for (identifier, kind) in &discovery.unresolved {
if *kind == super::walk::AssetKind::Asset {
return Err(Error::UnresolvedAsset(identifier.clone()));
}
skipped.push(identifier.clone());
}
let name = if let Some(name) = first_layer_name {
name.to_owned()
} else {
let location = discovery.sources[0]
.content
.location()
.ok_or_else(|| Error::UnresolvedAsset(asset_path.to_owned()))?;
ar::split_file_name(location).1.to_owned()
};
if !usdz::is_layer_name(&name) {
return Err(usdz::ArchiveError::InvalidEntryName {
name,
reason: "must name a native USD layer (.usd, .usda or .usdc) to be the package's default layer",
}
.into());
}
let entries = assign_entries(&discovery, &name, HashSet::new());
let mut temp = tf::SafeOutputFile::create(usdz_file_path.as_ref())?;
let mut archive = usdz::ArchiveWriter::new(temp.file());
write_entries(&graph, &discovery, &entries, &mut archive)?;
archive.finish()?;
temp.persist()?;
Ok(skipped)
}
fn assign_entries(discovery: &Discovery<'_>, first_name: &str, reserved: HashSet<String>) -> Vec<String> {
let mut entries: Vec<String> = Vec::with_capacity(discovery.sources.len());
let mut taken = reserved;
let mut directories = HashMap::new();
for source in &discovery.sources {
let entry = match &source.placement {
Placement::Root => first_name.to_owned(),
Placement::Entry(name) => name.clone(),
Placement::Named { by, path } => kept_entry(&entries[*by], path)
.filter(|entry| !taken.contains(entry))
.unwrap_or_else(|| moved_entry(&mut directories, &taken, &source.identifier)),
};
taken.insert(entry.clone());
entries.push(entry);
}
entries
}
fn moved_entry(directories: &mut HashMap<String, usize>, taken: &HashSet<String>, source: &str) -> String {
let (directory, name) = ar::split_file_name(source);
let next = directories.len();
let mut number = *directories.entry(directory.to_owned()).or_insert(next);
let mut entry = format!("{number}/{name}");
while taken.contains(&entry) {
number += 1;
entry = format!("{number}/{name}");
}
entry
}
fn write_entries<W: Write + Seek>(
graph: &pcp::LayerGraph,
discovery: &Discovery<'_>,
entries: &[String],
archive: &mut usdz::ArchiveWriter<W>,
) -> Result<()> {
for (index, source) in discovery.sources.iter().enumerate() {
let bytes = source_bytes(graph, source, &entries[index], entries, None)?;
archive.add_layer(&entries[index], &bytes)?;
}
Ok(())
}
fn source_bytes(
graph: &pcp::LayerGraph,
source: &Source<'_>,
location: &str,
entries: &[String],
original: Option<Vec<u8>>,
) -> Result<Vec<u8>> {
match &source.content {
Content::Layer { layer, uses } => {
let edits = visit_asset_paths(layer.data(), layer.identifier(), Visit::DISCOVER, |asset| {
Ok(rewrite(location, asset.path, uses, entries))
})?;
if edits.is_empty() {
serialize(layer.data(), location)
} else {
let mut data = sdf::Data::from_abstract(layer.data())?;
apply_edits(&mut data, edits);
serialize(&data, location)
}
}
Content::Package { resolved, inside } => {
let bytes = match original {
Some(bytes) => bytes,
None => read_asset(graph, resolved)?,
};
rebuild_package(graph, bytes, inside)
}
Content::Asset(resolved) => match original {
Some(bytes) => Ok(bytes),
None => read_asset(graph, resolved),
},
}
}
fn rebuild_package(graph: &pcp::LayerGraph, bytes: Vec<u8>, inside: &Discovery<'_>) -> Result<Vec<u8>> {
if inside.sources.is_empty() {
return Ok(bytes);
}
let mut archive = zip::ZipArchive::new(Cursor::new(bytes.as_slice())).map_err(usdz::ArchiveError::from)?;
let reserved = archive.file_names().map(str::to_owned).collect();
let entries = assign_entries(inside, "", reserved);
let existing: HashMap<&str, usize> = inside
.sources
.iter()
.enumerate()
.filter_map(|(index, source)| match &source.placement {
Placement::Entry(name) => Some((name.as_str(), index)),
Placement::Root | Placement::Named { .. } => None,
})
.collect();
let mut rebuilt = usdz::ArchiveWriter::new(Cursor::new(Vec::new()));
for index in 0..archive.len() {
let mut entry = archive.by_index(index).map_err(usdz::ArchiveError::from)?;
let name = entry.name().to_owned();
let mut contents = Vec::new();
entry.read_to_end(&mut contents)?;
let contents = match existing.get(name.as_str()) {
Some(&source) => source_bytes(graph, &inside.sources[source], &name, &entries, Some(contents))?,
None => contents,
};
rebuilt.add_layer(&name, &contents)?;
}
for (index, source) in inside.sources.iter().enumerate() {
if matches!(source.placement, Placement::Named { .. }) {
let contents = source_bytes(graph, source, &entries[index], &entries, None)?;
rebuilt.add_layer(&entries[index], &contents)?;
}
}
Ok(rebuilt.finish()?.into_inner())
}
fn rewrite(location: &str, path: &str, uses: &HashMap<String, Use>, entries: &[String]) -> Option<String> {
let Use { source, packaged } = uses.get(path)?;
let entry = &entries[*source];
let (to_source, authored) = match ar::split_package_relative_path_outer(path) {
Some((package, inner)) => (package, Some(inner)),
None => (path.to_owned(), None),
};
let as_authored = !to_source.contains('\\') && (packaged.is_none() || *packaged == authored);
if as_authored && kept_entry(location, &to_source).as_deref() == Some(entry) {
return None;
}
let moved = path_to(location, entry);
Some(match packaged {
Some(inner) => ar::join_package_relative_path(&moved, inner),
None => moved,
})
}
fn kept_entry(location: &str, path: &str) -> Option<String> {
if !ar::is_file_relative_path(path) {
return None;
}
let directory = location.rsplit_once('/').map_or("", |(directory, _)| directory);
ar::join_within_package(directory, path)
}
fn path_to(location: &str, entry: &str) -> String {
format!("{}{entry}", "../".repeat(location.matches('/').count()))
}
fn read_asset(graph: &pcp::LayerGraph, resolved: &ar::ResolvedPath) -> Result<Vec<u8>> {
Ok(graph.layer_registry().open_asset(resolved)?.read_all()?)
}
fn serialize(data: &dyn AbstractData, location: &str) -> Result<Vec<u8>> {
let mut bytes = Cursor::new(Vec::new());
sdf::LayerRegistry::export_format(location)?.write(data, &mut bytes)?;
Ok(bytes.into_inner())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use crate::ar::Resolver;
use crate::usd::stage::tests::attribute_value;
use crate::usd_utils::compute_all_dependencies;
use crate::usd_utils::discover::tests::{open, write};
use crate::usdz::ArchiveWriter;
fn entries(path: &Path) -> (Vec<String>, HashMap<String, String>) {
let mut archive = zip::ZipArchive::new(fs::File::open(path).unwrap()).unwrap();
let mut names = Vec::new();
let mut texts = HashMap::new();
for index in 0..archive.len() {
let mut entry = archive.by_index(index).unwrap();
let name = entry.name().to_owned();
if name.ends_with(".usda") {
let mut text = String::new();
entry.read_to_string(&mut text).unwrap();
texts.insert(name.clone(), text);
}
names.push(name);
}
(names, texts)
}
fn nested_entries(packaged: &str) -> (Vec<String>, HashMap<String, String>) {
let bytes = ar::DefaultResolver::new()
.open_asset(&ar::ResolvedPath::new(packaged))
.unwrap()
.read_all()
.unwrap();
let mut archive = zip::ZipArchive::new(Cursor::new(bytes)).unwrap();
let mut names = Vec::new();
let mut texts = HashMap::new();
for index in 0..archive.len() {
let mut entry = archive.by_index(index).unwrap();
let name = entry.name().to_owned();
if name.ends_with(".usda") {
let mut text = String::new();
entry.read_to_string(&mut text).unwrap();
texts.insert(name.clone(), text);
}
names.push(name);
}
(names, texts)
}
const ROOT: &str = r#"#usda 1.0
(
subLayers = [@./layers/s.usda@]
)
def "A" (
prepend references = @./sub/a.usda@</A>
)
{
}
def "B" (
prepend references = @../outside/b.usda@</B>
)
{
}
def "C" (
prepend references = @c.usda@</C>
)
{
}
def "M" (
prepend references = @./missing.usda@
)
{
}
def "N" (
prepend references = @../pk/inner.usdz[second.usda]@</Second>
)
{
}
def "T"
{
asset tex = @./tex/t.png@
}
"#;
#[test]
fn layout_matches_cpp() -> Result<()> {
let dir = tempfile::tempdir()?;
let base = dir.path();
write(base, "scene/root.usda", ROOT);
write(
base,
"scene/layers/s.usda",
"#usda 1.0\ndef \"S\" {\n int probe = 1\n}\n",
);
write(
base,
"scene/sub/a.usda",
"#usda 1.0\ndef \"A\" (\n prepend references = @../../outside/d.usda@</D>\n)\n{\n asset up = @../tex/t.png@\n}\n",
);
write(
base,
"outside/b.usda",
"#usda 1.0\ndef \"B\" {\n asset t = @../scene/tex/t.png@\n int probe = 3\n}\n",
);
write(base, "outside/d.usda", "#usda 1.0\ndef \"D\" {\n int probe = 4\n}\n");
write(base, "scene/c.usda", "#usda 1.0\ndef \"C\" {\n int probe = 5\n}\n");
write(base, "scene/tex/t.png", "PNG");
fs::create_dir_all(base.join("pk"))?;
let mut inner = ArchiveWriter::create(base.join("pk/inner.usdz"))?;
inner.add_layer("inner.usda", b"#usda 1.0\ndef \"Inner\" {\n}\n")?;
inner.add_layer("second.usda", b"#usda 1.0\ndef \"Second\" {\n int probe = 6\n}\n")?;
inner.finish()?;
let (root, stage) = open(base, "scene/root.usda")?;
let output = base.join("out.usdz");
let skipped = create_new_usdz_package(&stage, &root, &output, None)?;
assert_eq!(skipped.len(), 1);
assert!(Path::new(&skipped[0]).ends_with("scene/missing.usda"));
assert_eq!(
fs::read_dir(base)?.count(),
4,
"no staging file left beside the package"
);
let (names, texts) = entries(&output);
assert_eq!(
names,
[
"root.usda",
"layers/s.usda",
"sub/a.usda",
"0/b.usda",
"1/c.usda",
"2/inner.usdz",
"tex/t.png",
"0/d.usda",
]
);
let root_text = &texts["root.usda"];
for path in [
"@./layers/s.usda@",
"@./sub/a.usda@",
"@0/b.usda@",
"@1/c.usda@",
"@./missing.usda@",
"@2/inner.usdz[second.usda]@",
"@./tex/t.png@",
] {
assert!(root_text.contains(path), "{path} in {root_text}");
}
assert!(texts["sub/a.usda"].contains("@../0/d.usda@"));
assert!(texts["sub/a.usda"].contains("@../tex/t.png@"));
assert!(texts["0/b.usda"].contains("@../tex/t.png@"));
let packaged = Stage::open(output.to_str().unwrap())?;
assert_eq!(attribute_value(&packaged, "/S.probe"), Some(sdf::Value::Int(1)));
assert_eq!(attribute_value(&packaged, "/A.probe"), Some(sdf::Value::Int(4)));
assert_eq!(attribute_value(&packaged, "/B.probe"), Some(sdf::Value::Int(3)));
assert_eq!(attribute_value(&packaged, "/C.probe"), Some(sdf::Value::Int(5)));
assert_eq!(attribute_value(&packaged, "/N.probe"), Some(sdf::Value::Int(6)));
Ok(())
}
#[test]
fn unsaved_edits_first_layer_name() -> Result<()> {
let dir = tempfile::tempdir()?;
write(dir.path(), "root.usda", "#usda 1.0\ndef \"A\" {\n}\n");
let (root, stage) = open(dir.path(), "root.usda")?;
stage.define_prim("/Edited")?;
let output = dir.path().join("out.usdz");
create_new_usdz_package(&stage, &root, &output, Some("scene.usdc"))?;
assert_eq!(entries(&output).0, ["scene.usdc"]);
let packaged = Stage::open(output.to_str().unwrap())?;
assert!(packaged.prim("/Edited")?.is_valid()?);
Ok(())
}
#[test]
fn one_entry_per_source() -> Result<()> {
let dir = tempfile::tempdir()?;
write(
dir.path(),
"scene/root.usda",
"#usda 1.0\n(\n subLayers = [@weak.usda@]\n)\ndef \"A\" (\n delete references = @./ref.usda@</R>\n)\n{\n}\n",
);
write(
dir.path(),
"scene/weak.usda",
"#usda 1.0\ndef \"A\" (\n prepend references = @./ref.usda@</R>\n)\n{\n}\n",
);
write(
dir.path(),
"scene/ref.usda",
"#usda 1.0\ndef \"R\" {\n int probe = 42\n}\n",
);
let (root, stage) = open(dir.path(), "scene/root.usda")?;
assert_eq!(attribute_value(&stage, "/A.probe"), None);
let output = dir.path().join("out.usdz");
create_new_usdz_package(&stage, &root, &output, None)?;
let (names, texts) = entries(&output);
assert_eq!(names, ["root.usda", "0/weak.usda", "ref.usda"]);
assert!(
texts["0/weak.usda"].contains("@../ref.usda@"),
"{}",
texts["0/weak.usda"]
);
let packaged = Stage::open(output.to_str().unwrap())?;
assert_eq!(attribute_value(&packaged, "/A.probe"), None);
Ok(())
}
#[test]
fn first_layer_name_not_layer() -> Result<()> {
let dir = tempfile::tempdir()?;
write(dir.path(), "root.usda", "#usda 1.0\ndef \"A\" {\n}\n");
let (root, stage) = open(dir.path(), "root.usda")?;
let output = dir.path().join("out.usdz");
let error = create_new_usdz_package(&stage, &root, &output, Some("root.usdz")).unwrap_err();
assert!(
matches!(&error, Error::Archive(archive) if matches!(**archive, usdz::ArchiveError::InvalidEntryName { .. })),
"{error}"
);
assert!(!output.exists());
Ok(())
}
#[test]
fn missing_asset_fails() -> Result<()> {
let dir = tempfile::tempdir()?;
write(
dir.path(),
"root.usda",
"#usda 1.0\ndef \"A\" {\n asset t = @./missing.png@\n}\n",
);
let (root, stage) = open(dir.path(), "root.usda")?;
let output = dir.path().join("out.usdz");
let error = create_new_usdz_package(&stage, &root, &output, None).unwrap_err();
assert!(
matches!(&error, Error::UnresolvedAsset(path) if path.ends_with("missing.png")),
"{error}"
);
assert!(!output.exists());
Ok(())
}
#[test]
fn repackages_a_package() -> Result<()> {
let dir = tempfile::tempdir()?;
let source = dir.path().join("source.usdz");
let mut writer = ArchiveWriter::create(&source)?;
writer.add_layer(
"root.usda",
b"#usda 1.0\ndef \"A\" (\n prepend references = @./sub/inner.usda@</I>\n)\n{\n}\n",
)?;
writer.add_layer(
"sub/inner.usda",
b"#usda 1.0\ndef \"I\" {\n asset t = @../tex.png@\n int probe = 7\n}\n",
)?;
writer.add_layer("tex.png", b"PNG")?;
writer.finish()?;
let stage = Stage::open(source.to_str().unwrap())?;
let output = dir.path().join("out.usdz");
create_new_usdz_package(&stage, source.to_str().unwrap(), &output, None)?;
assert_eq!(entries(&output).0, ["root.usda", "sub/inner.usda", "tex.png"]);
let packaged = Stage::open(output.to_str().unwrap())?;
assert_eq!(attribute_value(&packaged, "/A.probe"), Some(sdf::Value::Int(7)));
Ok(())
}
#[test]
fn repackages_onto_itself() -> Result<()> {
let dir = tempfile::tempdir()?;
let source = dir.path().join("scene.usdz");
let mut writer = ArchiveWriter::create(&source)?;
writer.add_layer(
"root.usda",
b"#usda 1.0\ndef \"A\" {\n asset t = @./tex.png@\n int probe = 7\n}\n",
)?;
writer.add_layer("tex.png", b"PNG")?;
writer.finish()?;
let stage = Stage::open(source.to_str().unwrap())?;
assert_eq!(attribute_value(&stage, "/A.probe"), Some(sdf::Value::Int(7)));
create_new_usdz_package(&stage, source.to_str().unwrap(), &source, None)?;
assert_eq!(fs::read_dir(dir.path())?.count(), 1, "only the package is left");
assert_eq!(entries(&source).0, ["root.usda", "tex.png"]);
let packaged = Stage::open(source.to_str().unwrap())?;
assert_eq!(attribute_value(&packaged, "/A.probe"), Some(sdf::Value::Int(7)));
Ok(())
}
#[test]
fn nested_package_edits_kept() -> Result<()> {
let dir = tempfile::tempdir()?;
let mut writer = ArchiveWriter::create(dir.path().join("pkg.usdz"))?;
writer.add_layer("first.usda", b"#usda 1.0\ndef \"R\" {\n int probe = 1\n}\n")?;
writer.add_layer("other.usda", b"#usda 1.0\ndef \"R\" {\n int probe = 1\n}\n")?;
writer.add_layer("tex.png", b"PNG")?;
writer.finish()?;
write(
dir.path(),
"root.usda",
"#usda 1.0\ndef \"A\" (\n prepend references = @./pkg.usdz@</R>\n)\n{\n}\ndef \"B\" (\n prepend references = @./pkg.usdz[other.usda]@</R>\n)\n{\n}\n",
);
let (root, stage) = open(dir.path(), "root.usda")?;
assert_eq!(attribute_value(&stage, "/A.probe"), Some(sdf::Value::Int(1)));
assert_eq!(attribute_value(&stage, "/B.probe"), Some(sdf::Value::Int(1)));
let dependencies = compute_all_dependencies(&stage, &root)?;
let probe = sdf::path("/R.probe")?;
for identifier in dependencies.layers.iter().filter(|layer| layer.contains("pkg.usdz")) {
stage.layer_mut(identifier).unwrap().edit(|edit| {
edit.data_mut()
.set_field(&probe, sdf::FieldKey::Default.as_str(), sdf::Value::Int(42));
Ok(())
})?;
}
let output = dir.path().join("out.usdz");
create_new_usdz_package(&stage, &root, &output, None)?;
assert_eq!(entries(&output).0, ["root.usda", "pkg.usdz"]);
let nested = ar::join_package_relative_path(output.to_str().unwrap(), "pkg.usdz");
assert_eq!(nested_entries(&nested).0, ["first.usda", "other.usda", "tex.png"]);
let packaged = Stage::open(output.to_str().unwrap())?;
assert_eq!(attribute_value(&packaged, "/A.probe"), Some(sdf::Value::Int(42)));
assert_eq!(attribute_value(&packaged, "/B.probe"), Some(sdf::Value::Int(42)));
Ok(())
}
#[test]
fn nested_archives_kept() -> Result<()> {
let dir = tempfile::tempdir()?;
let mut inner = ArchiveWriter::new(Cursor::new(Vec::new()));
inner.add_layer(
"first.usda",
b"#usda 1.0\ndef \"A\" (\n prepend references = @./second.usda@</B>\n)\n{\n}\n",
)?;
inner.add_layer("second.usda", b"#usda 1.0\ndef \"B\" {\n int probe = 7\n}\n")?;
inner.add_layer("tex.png", b"PNG")?;
let inner = inner.finish()?.into_inner();
let mut outer = ArchiveWriter::create(dir.path().join("outer.usdz"))?;
outer.add_layer(
"root.usda",
b"#usda 1.0\ndef \"A\" (\n prepend references = @./inner.usdz@</A>\n)\n{\n}\n",
)?;
outer.add_layer("inner.usdz", &inner)?;
outer.finish()?;
write(
dir.path(),
"root.usda",
"#usda 1.0\ndef \"A\" (\n prepend references = @./outer.usdz@</A>\n)\n{\n}\n",
);
let (root, stage) = open(dir.path(), "root.usda")?;
assert_eq!(attribute_value(&stage, "/A.probe"), Some(sdf::Value::Int(7)));
let output = dir.path().join("out.usdz");
create_new_usdz_package(&stage, &root, &output, None)?;
let nested = ar::join_package_relative_path(output.to_str().unwrap(), "outer.usdz[inner.usdz]");
assert_eq!(nested_entries(&nested).0, ["first.usda", "second.usda", "tex.png"]);
let packaged = Stage::open(output.to_str().unwrap())?;
assert_eq!(attribute_value(&packaged, "/A.probe"), Some(sdf::Value::Int(7)));
Ok(())
}
#[test]
fn nested_layer_dependencies_packaged() -> Result<()> {
let dir = tempfile::tempdir()?;
write(dir.path(), "textures/t.png", "PNG");
let texture = dir.path().join("textures/t.png");
let texture = texture.to_str().unwrap().replace('\\', "/");
let mut writer = ArchiveWriter::create(dir.path().join("pkg.usdz"))?;
writer.add_layer(
"first.usda",
format!("#usda 1.0\ndef \"R\" {{\n asset tex = @{texture}@\n int probe = 1\n}}\n").as_bytes(),
)?;
writer.finish()?;
write(
dir.path(),
"root.usda",
"#usda 1.0\ndef \"A\" (\n prepend references = @./pkg.usdz@</R>\n)\n{\n}\n",
);
let (root, stage) = open(dir.path(), "root.usda")?;
assert_eq!(attribute_value(&stage, "/A.probe"), Some(sdf::Value::Int(1)));
let output = dir.path().join("out.usdz");
create_new_usdz_package(&stage, &root, &output, None)?;
let nested = ar::join_package_relative_path(output.to_str().unwrap(), "pkg.usdz");
let (names, texts) = nested_entries(&nested);
assert_eq!(names, ["first.usda", "0/t.png"]);
assert!(texts["first.usda"].contains("@0/t.png@"), "{}", texts["first.usda"]);
let packaged = Stage::open(output.to_str().unwrap())?;
let dependencies = compute_all_dependencies(&packaged, output.to_str().unwrap())?;
assert!(
dependencies
.assets
.iter()
.any(|asset| asset.ends_with("out.usdz[pkg.usdz[0/t.png]]")),
"{:?}",
dependencies.assets
);
assert!(dependencies.unresolved.is_empty(), "{:?}", dependencies.unresolved);
Ok(())
}
#[test]
fn nested_entry_names_reserved() -> Result<()> {
let dir = tempfile::tempdir()?;
write(dir.path(), "textures/extra.png", "NEW");
let texture = dir.path().join("textures/extra.png");
let texture = texture.to_str().unwrap().replace('\\', "/");
let mut writer = ArchiveWriter::create(dir.path().join("pkg.usdz"))?;
writer.add_layer(
"first.usda",
format!("#usda 1.0\ndef \"R\" {{\n asset tex = @{texture}@\n int probe = 1\n}}\n").as_bytes(),
)?;
writer.add_layer("0/extra.png", b"EXISTING")?;
writer.finish()?;
write(
dir.path(),
"root.usda",
"#usda 1.0\ndef \"A\" (\n prepend references = @./pkg.usdz@</R>\n)\n{\n}\n",
);
let (root, stage) = open(dir.path(), "root.usda")?;
assert_eq!(attribute_value(&stage, "/A.probe"), Some(sdf::Value::Int(1)));
let output = dir.path().join("out.usdz");
create_new_usdz_package(&stage, &root, &output, None)?;
let nested = ar::join_package_relative_path(output.to_str().unwrap(), "pkg.usdz");
let (names, texts) = nested_entries(&nested);
assert_eq!(names, ["first.usda", "0/extra.png", "1/extra.png"]);
assert!(texts["first.usda"].contains("@1/extra.png@"), "{}", texts["first.usda"]);
Ok(())
}
#[test]
fn deep_missing_asset_fails() -> Result<()> {
let dir = tempfile::tempdir()?;
let mut inner = ArchiveWriter::new(Cursor::new(Vec::new()));
inner.add_layer(
"first.usda",
b"#usda 1.0\ndef \"A\" {\n asset t = @./missing.png@\n int probe = 7\n}\n",
)?;
let inner = inner.finish()?.into_inner();
let mut outer = ArchiveWriter::create(dir.path().join("outer.usdz"))?;
outer.add_layer(
"root.usda",
b"#usda 1.0\ndef \"A\" (\n prepend references = @./inner.usdz@</A>\n)\n{\n}\n",
)?;
outer.add_layer("inner.usdz", &inner)?;
outer.finish()?;
write(
dir.path(),
"root.usda",
"#usda 1.0\ndef \"A\" (\n prepend references = @./outer.usdz@</A>\n)\n{\n}\n",
);
let (root, stage) = open(dir.path(), "root.usda")?;
assert_eq!(attribute_value(&stage, "/A.probe"), Some(sdf::Value::Int(7)));
let output = dir.path().join("out.usdz");
let error = create_new_usdz_package(&stage, &root, &output, None).unwrap_err();
assert!(
matches!(&error, Error::UnresolvedAsset(path) if path.ends_with("outer.usdz[inner.usdz[missing.png]]")),
"{error}"
);
assert!(!output.exists());
Ok(())
}
#[test]
fn absolute_packaged_paths_rewritten() -> Result<()> {
let dir = tempfile::tempdir()?;
let package = dir.path().join("pkg.usdz");
let absolute = package.to_str().unwrap().replace('\\', "/");
let mut inner = ArchiveWriter::new(Cursor::new(Vec::new()));
inner.add_layer("a.usda", b"#usda 1.0\ndef \"B\" {\n int deep = 9\n}\n")?;
let inner = inner.finish()?.into_inner();
let mut writer = ArchiveWriter::create(&package)?;
writer.add_layer(
"first.usda",
format!(
"#usda 1.0\ndef \"R\" (\n prepend references = @{absolute}[inner.usdz[a.usda]]@</B>\n)\n{{\n asset tex = @{absolute}[tex.png]@\n int probe = 1\n}}\n"
)
.as_bytes(),
)?;
writer.add_layer("inner.usdz", &inner)?;
writer.add_layer("tex.png", b"PNG")?;
writer.finish()?;
write(
dir.path(),
"root.usda",
"#usda 1.0\ndef \"A\" (\n prepend references = @./pkg.usdz@</R>\n)\n{\n}\n",
);
let (root, stage) = open(dir.path(), "root.usda")?;
assert_eq!(attribute_value(&stage, "/A.deep"), Some(sdf::Value::Int(9)));
let output = dir.path().join("out.usdz");
create_new_usdz_package(&stage, &root, &output, None)?;
fs::rename(&package, dir.path().join("moved.usdz"))?;
let nested = ar::join_package_relative_path(output.to_str().unwrap(), "pkg.usdz");
let (names, texts) = nested_entries(&nested);
assert_eq!(names, ["first.usda", "inner.usdz", "tex.png"]);
assert!(
texts["first.usda"].contains("@inner.usdz[a.usda]@"),
"{}",
texts["first.usda"]
);
assert!(texts["first.usda"].contains("@tex.png@"), "{}", texts["first.usda"]);
let packaged = Stage::open(output.to_str().unwrap())?;
assert_eq!(attribute_value(&packaged, "/A.deep"), Some(sdf::Value::Int(9)));
let dependencies = compute_all_dependencies(&packaged, output.to_str().unwrap())?;
assert!(dependencies.unresolved.is_empty(), "{:?}", dependencies.unresolved);
Ok(())
}
#[test]
fn backslash_paths_packaged() -> Result<()> {
let dir = tempfile::tempdir()?;
let base = dir.path();
write(
base,
"scene/root.usda",
"#usda 1.0\ndef \"T\"\n{\n asset search = @tex\\stone.png@\n asset kept = @.\\tex\\wood.png@\n}\n",
);
write(base, "scene/tex/stone.png", "PNG");
write(base, "scene/tex/wood.png", "PNG");
let (root, stage) = open(base, "scene/root.usda")?;
let dependencies = compute_all_dependencies(&stage, &root)?;
assert!(dependencies.unresolved.is_empty(), "{:?}", dependencies.unresolved);
for texture in ["stone.png", "wood.png"] {
let expected = fs::canonicalize(base.join("scene/tex").join(texture))?;
assert!(
dependencies.assets.iter().any(|asset| Path::new(asset) == expected),
"{texture}: {:?}",
dependencies.assets
);
}
let output = base.join("out.usdz");
let skipped = create_new_usdz_package(&stage, &root, &output, None)?;
assert!(skipped.is_empty(), "{skipped:?}");
let (names, texts) = entries(&output);
assert_eq!(names, ["root.usda", "tex/wood.png", "0/stone.png"]);
let root_text = &texts["root.usda"];
assert!(root_text.contains("@tex/wood.png@"), "{root_text}");
assert!(!root_text.contains('\\'), "{root_text}");
let packaged = Stage::open(output.to_str().unwrap())?;
for (attribute, entry) in [
("/T.search", "out.usdz[0/stone.png]"),
("/T.kept", "out.usdz[tex/wood.png]"),
] {
let asset = attribute_value(&packaged, attribute)
.and_then(|value| value.try_as_asset_path())
.expect("an asset value");
let resolved = asset.resolved_path().expect("the packaged texture resolves");
assert!(resolved.ends_with(entry), "{attribute}: {resolved}");
}
Ok(())
}
#[test]
fn backslash_packaged_part() -> Result<()> {
let dir = tempfile::tempdir()?;
let base = dir.path();
write(
base,
"scene/root.usda",
"#usda 1.0\ndef \"T\"\n{\n asset tex = @./model.usdz[tex\\stone.png]@\n}\n",
);
let mut model = ArchiveWriter::create(base.join("scene/model.usdz"))?;
model.add_layer("model.usda", b"#usda 1.0\ndef \"M\" {\n}\n")?;
model.add_layer("tex/stone.png", b"PNG")?;
model.finish()?;
let (root, stage) = open(base, "scene/root.usda")?;
let output = base.join("out.usdz");
let skipped = create_new_usdz_package(&stage, &root, &output, None)?;
assert!(skipped.is_empty(), "{skipped:?}");
let (names, texts) = entries(&output);
assert_eq!(names, ["root.usda", "model.usdz"]);
let root_text = &texts["root.usda"];
assert!(root_text.contains("@model.usdz[tex/stone.png]@"), "{root_text}");
let packaged = Stage::open(output.to_str().unwrap())?;
let asset = attribute_value(&packaged, "/T.tex")
.and_then(|value| value.try_as_asset_path())
.expect("an asset value");
let resolved = asset.resolved_path().expect("the packaged texture resolves");
assert!(resolved.ends_with("out.usdz[model.usdz[tex/stone.png]]"), "{resolved}");
Ok(())
}
#[test]
fn kept_entry_either_separator() {
assert_eq!(kept_entry("root.usda", r".\tex\a.png").as_deref(), Some("tex/a.png"));
assert_eq!(kept_entry("sub/a.usda", r"..\tex\a.png").as_deref(), Some("tex/a.png"));
assert_eq!(kept_entry("sub/a.usda", "./b/../c.usda").as_deref(), Some("sub/c.usda"));
assert_eq!(kept_entry("root.usda", r"..\out.png"), None, "leads out of the package");
assert_eq!(kept_entry("root.usda", r"tex\a.png"), None, "a search path");
}
#[test]
fn missing_packaged_entry() -> Result<()> {
let dir = tempfile::tempdir()?;
let mut package = ArchiveWriter::create(dir.path().join("assets.usdz"))?;
package.add_layer("a.usda", b"#usda 1.0\ndef \"A\" {\n}\n")?;
package.finish()?;
for (root, missing, layer) in [
(
"#usda 1.0\ndef \"A\" (\n prepend references = @./assets.usdz[missing.usda]@\n)\n{\n}\n",
"assets.usdz[missing.usda]",
true,
),
(
"#usda 1.0\ndef \"A\" {\n asset t = @./assets.usdz[missing.png]@\n}\n",
"assets.usdz[missing.png]",
false,
),
] {
write(dir.path(), "root.usda", root);
let (root, stage) = open(dir.path(), "root.usda")?;
let result = create_new_usdz_package(&stage, &root, dir.path().join("out.usdz"), None);
if layer {
let skipped = result?;
assert_eq!(skipped.len(), 1, "{skipped:?}");
assert!(skipped[0].ends_with(missing), "{skipped:?}");
} else {
let error = result.unwrap_err();
assert!(
matches!(&error, Error::UnresolvedAsset(path) if path.ends_with(missing)),
"{error}"
);
}
}
Ok(())
}
}