use std::fs;
use std::io::{self, Seek, Write};
use std::path::Path;
use zip::write::SimpleFileOptions;
use zip::{CompressionMethod, ZipWriter};
use super::ArchiveError;
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<(), ArchiveError> {
validate_entry_name(name)?;
let options = SimpleFileOptions::default()
.compression_method(CompressionMethod::Stored)
.with_alignment(USDZ_ALIGNMENT);
self.inner
.start_file(name, options)
.map_err(|e| ArchiveError::entry(name, e))?;
self.inner.write_all(bytes).map_err(|e| ArchiveError::entry(name, e))?;
Ok(())
}
pub fn finish(self) -> Result<W, ArchiveError> {
let w = self.inner.finish()?;
Ok(w)
}
}
impl ArchiveWriter<std::fs::File> {
pub fn create(path: impl AsRef<Path>) -> Result<Self, ArchiveError> {
let path = path.as_ref();
let file = fs::File::create(path)
.map_err(|error| io::Error::new(error.kind(), format!("unable to create {}: {error}", path.display())))?;
Ok(Self::new(file))
}
}
fn validate_entry_name(name: &str) -> Result<(), ArchiveError> {
let invalid = |reason: &'static str| ArchiveError::InvalidEntryName {
name: name.to_owned(),
reason,
};
if name.is_empty() {
return Err(invalid("cannot be empty"));
}
if name.contains('\\') {
return Err(invalid("must not contain backslashes"));
}
if name.starts_with('/') {
return Err(invalid("must be relative, not absolute"));
}
if name.ends_with('/') {
return Err(invalid("must name a file, not a directory"));
}
for seg in name.split('/') {
match seg {
"" => return Err(invalid("has an empty path segment")),
"." => return Err(invalid("must not contain `.` segments")),
".." => return Err(invalid("must not contain `..` 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);
}
}
}