mod package;
mod reader;
mod writer;
pub use package::{Package, PackageError, PackageStats, open_package, package_contains, package_entry, package_stats};
pub use reader::Archive;
pub(crate) use reader::is_layer_name;
pub use writer::ArchiveWriter;
use std::io::{self, Cursor};
use std::str;
use crate::{ar, sdf, tf, usda, usdc};
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ArchiveError {
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
Zip(#[from] zip::result::ZipError),
#[error("failed to access USDZ entry {name:?}")]
Entry {
name: String,
#[source]
source: Box<ArchiveError>,
},
#[error("no USD layer found in USDZ archive")]
NoDefaultLayer,
#[error("USDZ entry name {name:?} {reason}")]
InvalidEntryName {
name: String,
reason: &'static str,
},
#[error(transparent)]
Read(#[from] usdc::ReadError),
#[error(transparent)]
Parse(#[from] usda::ParseError),
#[error("file {name:?} is not valid UTF-8")]
Utf8 {
name: String,
#[source]
source: str::Utf8Error,
},
#[error("USDZ entry {name:?} is {method} compressed; only stored entries are read")]
Compressed {
name: String,
method: zip::CompressionMethod,
},
#[error("USDZ entry {name:?} is encrypted; only plain entries are read")]
Encrypted {
name: String,
},
}
impl ArchiveError {
pub(crate) fn entry(name: impl Into<String>, source: impl Into<ArchiveError>) -> Self {
Self::Entry {
name: name.into(),
source: Box::new(source.into()),
}
}
fn io_kind(&self) -> Option<io::ErrorKind> {
match self {
Self::Io(error) | Self::Zip(zip::result::ZipError::Io(error)) => Some(error.kind()),
Self::Entry { source, .. } => source.io_kind(),
_ => None,
}
}
}
pub struct UsdzFileFormat;
const USDZ_LAYER_NAME: &str = "layer.usdc";
impl sdf::FileFormat for UsdzFileFormat {
fn format_id(&self) -> tf::Token {
tf::Token::new("usdz")
}
fn extensions(&self) -> &[&str] {
&["usdz"]
}
fn caps(&self) -> sdf::FileFormatCaps {
sdf::FileFormatCaps::READ | sdf::FileFormatCaps::WRITE
}
fn is_package(&self) -> bool {
true
}
fn resolve_layer(&self, resolver: &dyn ar::Resolver, resolved: &ar::ResolvedPath) -> Option<ar::ResolvedPath> {
let package = resolved.to_string_lossy();
let first = open_package(resolver, resolved)
.ok()
.and_then(|package| package.with(|archive| archive.first_layer_name()));
Some(match first {
Some(first) => ar::ResolvedPath::new(ar::nest_packaged_path(&package, &first)),
None => resolved.clone(),
})
}
fn read_bytes(&self, bytes: ar::AssetBuffer, _source_name: &str) -> Result<sdf::LayerData, sdf::FormatError> {
Archive::from_bytes(bytes)
.and_then(|mut archive| archive.read_first_layer())
.map_err(|error| sdf::FormatError::Decode(Box::new(error)))
}
fn read(
&self,
resolver: &dyn ar::Resolver,
resolved: &ar::ResolvedPath,
) -> Result<sdf::LayerData, sdf::FormatError> {
let package = open_package(resolver, resolved).map_err(|error| match error.archive_error() {
ArchiveError::Io(_) => sdf::FormatError::Io(error.into()),
_ => sdf::FormatError::Decode(Box::new(error)),
})?;
package
.with(|archive| archive.read_first_layer())
.map_err(|error| sdf::FormatError::Decode(Box::new(error)))
}
fn write(&self, data: &dyn sdf::AbstractData, sink: &mut dyn sdf::WriteSeek) -> Result<(), sdf::FormatError> {
let encode = |error: ArchiveError| match error.io_kind() {
Some(kind) => sdf::FormatError::Io(io::Error::new(kind, error)),
None => sdf::FormatError::Encode {
reason: tf::error_chain(&error).into(),
},
};
let mut buf = Vec::new();
usdc::CrateWriter::write(data, &mut Cursor::new(&mut buf))?;
let mut archive = ArchiveWriter::new(sink);
archive.add_layer(USDZ_LAYER_NAME, &buf).map_err(encode)?;
archive.finish().map_err(encode)?;
Ok(())
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::Result;
use crate::ar::Resolver;
use crate::ar::tests::TestResolver;
use crate::sdf::FileFormat;
use crate::usd::{PrimPredicate, Stage, TimeCode};
use std::fs;
use std::io::Write;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
fn failing_resolver() -> impl ar::Resolver {
TestResolver(|| -> io::Result<Box<dyn ar::Asset>> { Err(io::Error::from(io::ErrorKind::PermissionDenied)) })
}
struct CountingAsset {
inner: Cursor<Vec<u8>>,
read: Arc<AtomicUsize>,
}
impl io::Read for CountingAsset {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let count = self.inner.read(buf)?;
self.read.fetch_add(count, Ordering::Relaxed);
Ok(count)
}
}
impl io::Seek for CountingAsset {
fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
self.inner.seek(pos)
}
}
impl ar::Asset for CountingAsset {
fn size(&self) -> io::Result<u64> {
self.inner.size()
}
}
struct FailingSink {
tripped: bool,
}
impl io::Write for FailingSink {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
if self.tripped {
return Ok(buf.len());
}
self.tripped = true;
Err(io::Error::from(io::ErrorKind::BrokenPipe))
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl io::Seek for FailingSink {
fn seek(&mut self, _pos: io::SeekFrom) -> io::Result<u64> {
Ok(0)
}
}
#[test]
fn read_io_stays_io() {
let Err(error) = UsdzFileFormat.read(&failing_resolver(), &ar::ResolvedPath::new("pkg.usdz")) else {
panic!("asset open fails");
};
let sdf::FormatError::Io(error) = error else {
panic!("a storage failure must stay I/O, got: {error}");
};
assert_eq!(error.kind(), io::ErrorKind::PermissionDenied);
}
#[test]
fn write_io_stays_io() {
let mut data = sdf::Data::new();
data.create_spec(sdf::Path::abs_root(), sdf::SpecType::PseudoRoot);
let mut sink = FailingSink { tripped: false };
let error = UsdzFileFormat.write(&data, &mut sink).expect_err("sink writes fail");
let sdf::FormatError::Io(error) = error else {
panic!("a storage failure must stay I/O, got: {error}");
};
assert_eq!(error.kind(), io::ErrorKind::BrokenPipe);
}
#[test]
fn resolves_packaged_reference() -> Result<()> {
let root =
b"#usda 1.0\n(defaultPrim = \"World\")\ndef \"World\" (prepend references = @./inner.usda@</Inner>) {}\n";
let inner = b"#usda 1.0\ndef \"Inner\" { custom int probe = 42 }\n";
let dir = tempfile::tempdir()?;
let path = dir.path().join("pkg.usdz");
let mut writer = ArchiveWriter::create(&path)?;
writer.add_layer("root.usda", root)?; writer.add_layer("inner.usda", inner)?;
writer.finish()?;
let stage = Stage::open(path.to_str().unwrap())?;
assert_eq!(
stage
.attribute("/World.probe")?
.get_at::<sdf::Value>(TimeCode::new(0.0))?,
Some(sdf::Value::Int(42)),
"reference to a layer inside the package should compose"
);
Ok(())
}
#[test]
fn default_layer_central_directory() -> Result<()> {
let mut writer = ArchiveWriter::new(Cursor::new(Vec::new()));
writer.add_layer("root.usda", b"#usda 1.0\n")?;
writer.add_layer("texture.bin", &vec![0; 1 << 20])?;
let package = writer.finish()?.into_inner();
let size = package.len();
let read = Arc::new(AtomicUsize::new(0));
let resolver = TestResolver({
let read = read.clone();
move || -> io::Result<Box<dyn ar::Asset>> {
Ok(Box::new(CountingAsset {
inner: Cursor::new(package.clone()),
read: read.clone(),
}))
}
});
let resolved = UsdzFileFormat.resolve_layer(&resolver, &ar::ResolvedPath::new("pkg.usdz"));
assert_eq!(resolved, Some(ar::ResolvedPath::new("pkg.usdz[root.usda]")));
let read = read.load(Ordering::Relaxed);
assert!(read < size / 4, "read {read} of {size} bytes");
Ok(())
}
#[test]
fn shared_buffer_read() -> Result<()> {
let mut writer = ArchiveWriter::new(Cursor::new(Vec::new()));
writer.add_layer("root.usda", b"#usda 1.0\ndef \"Root\" {}\n")?;
let package: Arc<[u8]> = writer.finish()?.into_inner().into();
let resolver = TestResolver({
let package = ar::AssetBuffer::from(package.clone());
move || -> io::Result<Box<dyn ar::Asset>> { Ok(Box::new(Cursor::new(package.clone()))) }
});
let data = UsdzFileFormat.read(&resolver, &ar::ResolvedPath::new("pkg.usdz"))?;
assert_eq!(data.spec_type(&sdf::path("/Root").unwrap()), Some(sdf::SpecType::Prim));
Ok(())
}
#[test]
fn entry_is_view() -> Result<()> {
let layer = b"#usda 1.0\ndef \"Root\" {}\n";
let mut writer = ArchiveWriter::new(Cursor::new(Vec::new()));
writer.add_layer("root.usda", layer)?;
let package = writer.finish()?.into_inner();
let bounds = package.as_ptr_range();
let mut archive = Archive::from_bytes(package)?;
let ar::AssetBuffer::Shared(view) = archive.entry("root.usda")? else {
panic!("a package in hand serves views");
};
assert!(
bounds.start <= view.as_ptr() && view.as_ptr_range().end <= bounds.end,
"the view lies inside the package"
);
assert_eq!(&*view, layer);
Ok(())
}
#[test]
fn entry_bounded_read() -> Result<()> {
let mut writer = ArchiveWriter::new(Cursor::new(Vec::new()));
writer.add_layer("root.usda", b"#usda 1.0\n")?;
writer.add_layer("texture.bin", &vec![0; 1 << 20])?;
let package = writer.finish()?.into_inner();
let size = package.len();
let read = Arc::new(AtomicUsize::new(0));
let mut archive = Archive::from_asset(Box::new(CountingAsset {
inner: Cursor::new(package),
read: read.clone(),
}))?;
assert!(archive.contains("root.usda"));
assert!(archive.contains("texture.bin"));
assert!(!archive.contains("missing.usda"));
assert_eq!(archive.first_layer_name().as_deref(), Some("root.usda"));
let probed = read.load(Ordering::Relaxed);
assert!(probed < size / 4, "the probes read {probed} of {size} bytes");
let ar::AssetBuffer::Owned(bytes) = archive.entry("root.usda")? else {
panic!("an asset without shared bytes is read");
};
assert_eq!(bytes, b"#usda 1.0\n");
let entry = read.load(Ordering::Relaxed) - probed;
assert!(entry < 4096, "the entry read {entry} bytes");
Ok(())
}
#[test]
fn compressed_entry_rejected() -> Result<()> {
let mut zip = zip::ZipWriter::new(Cursor::new(Vec::new()));
let deflated = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
zip.start_file("root.usda", deflated).map_err(ArchiveError::from)?;
zip.write_all(b"#usda 1.0\n")?;
let package = zip.finish().map_err(ArchiveError::from)?.into_inner();
let error = Archive::from_bytes(package)?
.entry("root.usda")
.expect_err("a deflated entry is refused");
assert!(matches!(error, ArchiveError::Compressed { .. }), "{error:?}");
Ok(())
}
#[test]
fn corrupt_entry() -> Result<()> {
let layer = b"#usda 1.0\ndef \"Root\" {}\n";
let mut writer = ArchiveWriter::new(Cursor::new(Vec::new()));
writer.add_layer("root.usda", layer)?;
let mut package = writer.finish()?.into_inner();
let offset = package
.windows(layer.len())
.position(|window| window == layer)
.expect("the stored entry");
package[offset + 1] ^= 0xff;
let mut archive = Archive::from_bytes(package.clone())?;
assert_eq!(archive.entry("root.usda")?[1], layer[1] ^ 0xff);
let error = archive.verify("root.usda").expect_err("the checksum no longer matches");
assert!(format!("{error:?}").to_lowercase().contains("checksum"), "{error:?}");
let mut archive = Archive::from_asset(Box::new(Cursor::new(package)))?;
assert!(
archive.entry("root.usda").is_err(),
"a checked read reports the corruption"
);
Ok(())
}
#[test]
fn nested_entry_is_view() -> Result<()> {
let mut inner = ArchiveWriter::new(Cursor::new(Vec::new()));
inner.add_layer("inner.usda", b"#usda 1.0\n")?;
let inner = inner.finish()?.into_inner();
let mut outer = ArchiveWriter::new(Cursor::new(Vec::new()));
outer.add_layer("root.usda", b"#usda 1.0\n")?;
outer.add_layer("inner.usdz", &inner)?;
let outer = outer.finish()?.into_inner();
let bounds = outer.as_ptr_range();
let mut archive = Archive::from_bytes(outer)?;
let mut nested = Archive::from_bytes(archive.entry("inner.usdz")?)?;
let ar::AssetBuffer::Shared(view) = nested.entry("inner.usda")? else {
panic!("a nested package serves views");
};
assert!(
bounds.start <= view.as_ptr() && view.as_ptr_range().end <= bounds.end,
"the view lies inside the outer package"
);
assert_eq!(&*view, b"#usda 1.0\n");
Ok(())
}
#[test]
fn stage_over_package() -> Result<()> {
let path = concat!(
env!("CARGO_WORKSPACE_DIR"),
"vendor/usd-wg-assets/full_assets/CarbonFrameBike/CarbonFrameBike.usdz"
);
if fs::metadata(path).is_err() {
eprintln!("Skipping stage_over_package: fixture not available at {path}");
return Ok(());
}
let stage = Stage::open(path)?;
let mut prims = 0;
stage.traverse(PrimPredicate::DEFAULT, |_| prims += 1)?;
assert!(prims > 100, "{prims} prims");
Ok(())
}
#[test]
fn resolves_nested_package() -> Result<()> {
let mut inner = ArchiveWriter::new(Cursor::new(Vec::new()));
inner.add_layer("inner.usda", b"#usda 1.0\ndef \"Inner\" { custom int probe = 42 }\n")?;
let inner = inner.finish()?.into_inner();
let root = b"#usda 1.0\ndef \"World\" (prepend references = @./inner.usdz@</Inner>) {}\n";
let dir = tempfile::tempdir()?;
let path = dir.path().join("outer.usdz");
let mut writer = ArchiveWriter::create(&path)?;
writer.add_layer("root.usda", root)?;
writer.add_layer("inner.usdz", &inner)?;
writer.finish()?;
let probe = |stage: &Stage, attr: &str| -> Result<Option<sdf::Value>> {
stage.attribute(attr)?.get_at::<sdf::Value>(TimeCode::new(0.0))
};
let stage = Stage::open(path.to_str().unwrap())?;
assert_eq!(probe(&stage, "/World.probe")?, Some(sdf::Value::Int(42)));
let nested = ar::join_package_relative_path(path.to_str().unwrap(), "inner.usdz");
let stage = Stage::open(&nested)?;
assert_eq!(probe(&stage, "/Inner.probe")?, Some(sdf::Value::Int(42)));
let data = Archive::open(&path)?.read("inner.usdz")?;
assert!(data.has_spec(&sdf::path("/Inner")?));
let resolver = ar::DefaultResolver::new();
for missing in ["inner.usdz[missing.usda]", "missing.usdz[inner.usda]"] {
let nested = ar::join_package_relative_path(path.to_str().unwrap(), missing);
assert_eq!(resolver.resolve(&nested), None, "{nested}");
}
Ok(())
}
pub(crate) fn package(entries: &[(&str, &[u8])]) -> Vec<u8> {
let mut writer = ArchiveWriter::new(Cursor::new(Vec::new()));
for (name, bytes) in entries {
writer.add_layer(name, bytes).expect("a valid entry");
}
writer.finish().expect("a finished package").into_inner()
}
}