use std::io::{Seek, Write};
use std::path::Path;
use anyhow::{bail, Context, Result};
use zip::write::SimpleFileOptions;
use zip::{CompressionMethod, ZipWriter};
const USDZ_ALIGNMENT: u16 = 64;
pub struct ArchiveWriter<W: Write + Seek> {
inner: ZipWriter<W>,
}
impl<W: Write + Seek> ArchiveWriter<W> {
pub fn new(out: W) -> Self {
Self {
inner: ZipWriter::new(out),
}
}
pub fn add_layer(&mut self, name: &str, bytes: &[u8]) -> Result<()> {
validate_entry_name(name)?;
let options = SimpleFileOptions::default()
.compression_method(CompressionMethod::Stored)
.with_alignment(USDZ_ALIGNMENT);
self.inner
.start_file(name, options)
.with_context(|| format!("failed to start USDZ entry {name}"))?;
self.inner
.write_all(bytes)
.with_context(|| format!("failed to write USDZ entry {name}"))?;
Ok(())
}
pub fn finish(self) -> Result<W> {
let w = self.inner.finish().context("failed to finalize USDZ archive")?;
Ok(w)
}
}
impl ArchiveWriter<std::fs::File> {
pub fn create(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
let file = std::fs::File::create(path)
.with_context(|| format!("failed to create USDZ archive: {}", path.display()))?;
Ok(Self::new(file))
}
}
fn validate_entry_name(name: &str) -> Result<()> {
if name.is_empty() {
bail!("USDZ entry name cannot be empty");
}
if name.contains('\\') {
bail!("USDZ entry name {name:?} must not contain backslashes");
}
if name.starts_with('/') {
bail!("USDZ entry name {name:?} must be relative, not absolute");
}
if name.ends_with('/') {
bail!("USDZ entry name {name:?} must name a file, not a directory");
}
for seg in name.split('/') {
match seg {
"" => bail!("USDZ entry name {name:?} has an empty path segment"),
"." | ".." => {
bail!("USDZ entry name {name:?} must not contain `{seg}` segments")
}
_ => {}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
fn writer() -> ArchiveWriter<Cursor<Vec<u8>>> {
ArchiveWriter::new(Cursor::new(Vec::new()))
}
#[test]
fn rejects_unsafe_entry_names() {
for name in [
"",
"/absolute",
"..",
"a/../b",
"win\\style",
"a//b",
"a/",
"./a",
"a/.",
] {
assert!(
writer().add_layer(name, b"").is_err(),
"expected rejection for {name:?}"
);
}
}
#[test]
fn accepts_safe_relative_entry_names() {
for name in ["scene.usdc", "Textures/Material/base.jpg", "nested/a.b.c"] {
writer().add_layer(name, b"dummy").expect(name);
}
}
}