mod reader;
mod writer;
pub use reader::Archive;
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("Nested USDZ files are not yet supported: '{path}'")]
NestedPackage {
path: String,
},
#[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,
},
}
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 resolve_layer(&self, resolver: &dyn ar::Resolver, resolved: &ar::ResolvedPath) -> Option<ar::ResolvedPath> {
let package = resolved.to_string_lossy();
if ar::is_package_relative_path(&package) {
return Some(resolved.clone());
}
match Archive::from_asset(resolver, resolved)
.ok()
.and_then(|a| a.first_layer_name())
{
Some(first) => Some(ar::ResolvedPath::new(ar::join_package_relative_path(&package, &first))),
None => Some(resolved.clone()),
}
}
fn read(
&self,
resolver: &dyn ar::Resolver,
resolved: &ar::ResolvedPath,
) -> Result<sdf::LayerData, sdf::FormatError> {
let s = resolved.to_string();
if let Some((_, inner)) = ar::split_package_relative_path_outer(&s) {
return Err(sdf::FormatError::Decode(Box::new(ArchiveError::NestedPackage {
path: inner,
})));
}
Archive::from_asset(resolver, resolved)
.and_then(|mut archive| archive.read_first_layer())
.map_err(|error| match error {
ArchiveError::Io(error) => sdf::FormatError::Io(error),
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)]
mod tests {
use super::*;
use crate::Result;
use crate::sdf::FileFormat;
use crate::usd::{Stage, TimeCode};
struct FailingResolver;
impl ar::Resolver for FailingResolver {
fn create_identifier(&self, asset_path: &str, _anchor: Option<&ar::ResolvedPath>) -> String {
asset_path.to_string()
}
fn resolve(&self, asset_path: &str) -> Option<ar::ResolvedPath> {
Some(ar::ResolvedPath::new(asset_path))
}
fn resolve_for_new_asset(&self, asset_path: &str) -> Option<ar::ResolvedPath> {
Some(ar::ResolvedPath::new(asset_path))
}
fn open_asset(&self, _resolved_path: &ar::ResolvedPath) -> io::Result<Box<dyn ar::Asset>> {
Err(io::Error::from(io::ErrorKind::PermissionDenied))
}
}
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(&FailingResolver, &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(())
}
}