use std::fs::File;
use std::io::{self, Cursor, Read};
use std::path::Path;
use std::str;
use zip::{CompressionMethod, ZipArchive};
use super::ArchiveError;
use crate::{ar, sdf, usda, usdc};
pub struct Archive {
archive: ZipArchive<Box<dyn ar::Asset>>,
bytes: Option<ar::SharedBuffer>,
}
impl Archive {
pub fn open(path: impl AsRef<Path>) -> Result<Self, ArchiveError> {
let path = path.as_ref();
let file = File::open(path)
.map_err(|error| io::Error::new(error.kind(), format!("unable to open {}: {error}", path.display())))?;
Self::from_asset(Box::new(file))
}
pub fn from_asset(asset: Box<dyn ar::Asset>) -> Result<Self, ArchiveError> {
let bytes = asset.shared_buffer();
let archive = ZipArchive::new(asset)?;
Ok(Archive { archive, bytes })
}
pub fn from_bytes(bytes: impl Into<ar::AssetBuffer>) -> Result<Self, ArchiveError> {
let bytes = bytes.into().into_shared();
Self::from_asset(Box::new(Cursor::new(ar::AssetBuffer::Shared(bytes))))
}
pub fn contains(&self, name: &str) -> bool {
self.archive.index_for_name(name).is_some()
}
pub fn first_layer_name(&self) -> Option<String> {
self.archive
.file_names()
.find(|name| is_layer_name(name))
.map(String::from)
}
pub fn read_first_layer(&mut self) -> Result<Box<dyn sdf::AbstractData>, ArchiveError> {
let name = self.first_layer_name().ok_or(ArchiveError::NoDefaultLayer)?;
self.read(&name)
}
pub fn entry(&mut self, name: &str) -> Result<ar::AssetBuffer, ArchiveError> {
let index = self
.archive
.index_for_name(name)
.ok_or_else(|| ArchiveError::entry(name, zip::result::ZipError::FileNotFound))?;
let (start, size) = {
let file = self
.archive
.by_index_raw(index)
.map_err(|error| ArchiveError::entry(name, error))?;
if file.encrypted() {
return Err(ArchiveError::Encrypted { name: name.to_owned() });
}
if file.compression() != CompressionMethod::Stored {
return Err(ArchiveError::Compressed {
name: name.to_owned(),
method: file.compression(),
});
}
if file.compressed_size() != file.size() {
return Err(ArchiveError::entry(
name,
io::Error::new(io::ErrorKind::InvalidData, "stored entry sizes disagree"),
));
}
(file.data_start(), file.size())
};
let Some(bytes) = &self.bytes else {
let mut file = self
.archive
.by_name(name)
.map_err(|error| ArchiveError::entry(name, error))?;
let mut buffer = Vec::new();
buffer
.try_reserve_exact(usize::try_from(size).unwrap_or(usize::MAX))
.map_err(|_| ArchiveError::entry(name, io::Error::from(io::ErrorKind::OutOfMemory)))?;
file.read_to_end(&mut buffer)
.map_err(|error| ArchiveError::entry(name, error))?;
return Ok(ar::AssetBuffer::Owned(buffer));
};
let range = start
.and_then(|start| usize::try_from(start).ok())
.zip(usize::try_from(size).ok())
.and_then(|(start, size)| Some(start..start.checked_add(size)?))
.and_then(|range| bytes.slice(range));
match range {
Some(view) => Ok(ar::AssetBuffer::Shared(view)),
None => Err(ArchiveError::entry(name, io::Error::from(io::ErrorKind::UnexpectedEof))),
}
}
pub fn verify(&mut self, name: &str) -> Result<(), ArchiveError> {
let mut file = self
.archive
.by_name(name)
.map_err(|error| ArchiveError::entry(name, error))?;
io::copy(&mut file, &mut io::sink()).map_err(|error| ArchiveError::entry(name, error))?;
Ok(())
}
pub fn read(&mut self, file_path: &str) -> Result<Box<dyn sdf::AbstractData>, ArchiveError> {
let bytes = self.entry(file_path)?;
if file_path.ends_with(".usdz") {
return Archive::from_bytes(bytes)
.and_then(|mut nested| nested.read_first_layer())
.map_err(|e| ArchiveError::entry(file_path, e));
}
let is_crate = if file_path.ends_with(".usdc") {
true
} else if file_path.ends_with(".usda") {
false
} else {
bytes.starts_with(usdc::MAGIC)
};
if is_crate {
let data = usdc::CrateData::open(bytes, true).map_err(|e| ArchiveError::entry(file_path, e))?;
Ok(Box::new(data))
} else {
let content = str::from_utf8(&bytes).map_err(|source| ArchiveError::Utf8 {
name: file_path.to_owned(),
source,
})?;
let data = usda::parse(content).map_err(|error| error.with_source_name(file_path))?;
Ok(Box::new(data))
}
}
}
pub(crate) fn is_layer_name(name: &str) -> bool {
sdf::LayerRegistry::find_by_extension(ar::extension(name)).is_some_and(|format| !format.is_package())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Result;
#[test]
fn test_open_usdz() -> Result<()> {
let mut archive = Archive::open("fixtures/test.usdz")?;
let data = archive.read("file_1.usdc")?;
let root = sdf::Path::abs_root();
assert!(data.has_spec(&root));
assert_eq!(data.spec_type(&root), Some(sdf::SpecType::PseudoRoot));
Ok(())
}
}