//! Xz-compressed tarball archive codec. use crate::archive::{Archive, ArchiveWriter, MAX_DECOMPRESSED_SIZE, checked_entry_size}; use crate::{AsyncContainer, ContainerError, Result, validate_path}; use lzma_rust2::{XzOptions, XzReader, XzWriter as InnerXzWriter}; use std::io::{Read, Seek, Write}; pub struct Xz; /// xz-tar writer. Held as an `Option` so `finish` can take ownership and unwind the layered /// writers in the right order: drop the tar builder first to flush its trailer, then finish xz. pub struct XzWriter { tar: Option>>, } impl Archive for Xz { type Writer = XzWriter; fn writer(output: W) -> Result> { let xz_writer = InnerXzWriter::new(output, XzOptions::default()).map_err(lzma_err)?; Ok(XzWriter { tar: Some(tar::Builder::new(xz_writer)), }) } fn open(source: R, dest: &mut C) -> Result<()> { // `take` bounds how many bytes we decompress from the xz stream, but each tar entry's declared // size is fed to `write_sized`, which pre-allocates from it before reading. Cap the cumulative // declared size too so a header claiming a huge size can't trigger a giant allocation up front. let xz_reader = XzReader::new(source, false); let bounded = xz_reader.take(MAX_DECOMPRESSED_SIZE); let mut tar_reader = tar::Archive::new(bounded); let mut total_size = 0u64; for entry in tar_reader.entries()? { let mut entry = entry?; if entry.header().entry_type() != tar::EntryType::Regular { continue; } // Reject non-UTF8 entry names rather than lossily rewriting them, so the path we store matches // the archive exactly. let path = entry.path()?; let path = path.to_str().ok_or_else(|| ContainerError::Codec(format!("tar: non-UTF8 entry name {path:?}")))?.to_owned(); validate_path(&path)?; let size = checked_entry_size(&mut total_size, entry.size())?; dest.write_sized_non_blocking(&path, size, &mut |buffer| { entry.read_exact(buffer).map_err(ContainerError::Io)?; Ok(()) })?; } Ok(()) } } impl ArchiveWriter for XzWriter { type Sink = W; fn write_entry(&mut self, path: &str, bytes: &[u8]) -> Result<()> { validate_path(path)?; let tar = self.tar.as_mut().ok_or_else(|| ContainerError::Codec("XzWriter already finished".into()))?; let mut header = tar::Header::new_gnu(); header.set_path(path).map_err(|error| ContainerError::Codec(format!("tar: invalid path {path}: {error}")))?; header.set_size(bytes.len() as u64); header.set_mode(0o644); header.set_cksum(); tar.append(&header, bytes)?; Ok(()) } fn finish_into(mut self) -> Result { self.finish_inner() } } impl XzWriter { /// Unwind the layered writers in order (flush the tar trailer, then finish xz) and hand back the /// innermost sink. fn finish_inner(&mut self) -> Result { let mut tar = self.tar.take().ok_or_else(|| ContainerError::Codec("XzWriter already finished".into()))?; tar.finish()?; let xz_writer = tar.into_inner()?; xz_writer.finish().map_err(lzma_err) } } fn lzma_err(error: std::io::Error) -> ContainerError { ContainerError::Codec(format!("lzma: {error}")) }