mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-22 21:48:12 +08:00
Add document-container crate (#4191)
* Add document-container crate: container backends and archive codecs * Address PR review: path/prefix split, safe size casts, OPFS stream aborts * Address PR review round 2: mmap read check, UTF8 entry names, prefix normalization * Make MmappedBytes::new fallible so mmap reads can't silently degrade * Address PR review round 3: backend contract uniformity (symlinks, remove, list) * Apply symlink-component check to FolderBackend listing paths * Address PR review: idempotent OPFS delete logging, tar default-features, shared entry-size cap * Fix validate_path doc: dotfiles pass, CurDir/ParentDir rejected * Omit symlink entries from FolderBackend listings for consistency with resolve * Preserve zip I/O errors and reject non-canonical paths in validate_path * Extend archive apis to return the archive writer * Rename document/document-container directory to document/container * Add archive format sniffing and deserialize_auto * Drop temporal hedge from checked_entry_size comment * Tighten verbose doc comments in document-container * Coalesce consecutive same-path OPFS appends to avoid O(n^2) file copies * Review * Update document-container for the deserialize/store_non_blocking renames --------- Co-authored-by: Timon <me@timon.zip>
This commit is contained in:
committed by
Keavon Chambers
co-authored by
Timon
parent
08c6d02e5b
commit
6fe1af3afe
@@ -0,0 +1,97 @@
|
||||
//! Xz-compressed tarball archive codec.
|
||||
|
||||
use crate::archive::{Archive, ArchiveWriter, MAX_DECOMPRESSED_SIZE, checked_entry_size};
|
||||
use crate::{Container, 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<W: Write + Seek> {
|
||||
tar: Option<tar::Builder<InnerXzWriter<W>>>,
|
||||
}
|
||||
|
||||
impl Archive for Xz {
|
||||
type Writer<W: Write + Seek> = XzWriter<W>;
|
||||
|
||||
fn writer<W: Write + Seek>(output: W) -> Result<Self::Writer<W>> {
|
||||
let xz_writer = InnerXzWriter::new(output, XzOptions::default()).map_err(lzma_err)?;
|
||||
Ok(XzWriter {
|
||||
tar: Some(tar::Builder::new(xz_writer)),
|
||||
})
|
||||
}
|
||||
|
||||
fn open<R: Read + Seek, C: Container>(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(&path, size, &mut |buffer| {
|
||||
entry.read_exact(buffer).map_err(ContainerError::Io)?;
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write + Seek> ArchiveWriter for XzWriter<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(mut self) -> Result<()> {
|
||||
self.finish_inner()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write + Seek> XzWriter<W> {
|
||||
/// Finish the archive and return the underlying sink, for in-memory archives where the caller
|
||||
/// wants the written bytes (e.g. `Cursor<Vec<u8>>`) back.
|
||||
pub fn finish_into(mut self) -> Result<W> {
|
||||
self.finish_inner()
|
||||
}
|
||||
|
||||
/// Unwind the layered writers in order (flush the tar trailer, then finish xz) and hand back the
|
||||
/// innermost sink. Shared by `finish` and `finish_into`.
|
||||
fn finish_inner(&mut self) -> Result<W> {
|
||||
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}"))
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
//! Zip archive codec.
|
||||
|
||||
use crate::archive::{Archive, ArchiveWriter, checked_entry_size};
|
||||
use crate::{Container, ContainerError, Result, validate_path};
|
||||
use std::io::{Read, Seek, Write};
|
||||
|
||||
use zip::ZipArchive;
|
||||
use zip::write::{SimpleFileOptions, ZipWriter as InnerZipWriter};
|
||||
|
||||
pub struct Zip;
|
||||
|
||||
pub struct ZipWriter<W: Write + Seek> {
|
||||
inner: InnerZipWriter<W>,
|
||||
options: SimpleFileOptions,
|
||||
}
|
||||
|
||||
impl Archive for Zip {
|
||||
type Writer<W: Write + Seek> = ZipWriter<W>;
|
||||
|
||||
fn writer<W: Write + Seek>(output: W) -> Result<Self::Writer<W>> {
|
||||
Ok(ZipWriter {
|
||||
inner: InnerZipWriter::new(output),
|
||||
options: SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated),
|
||||
})
|
||||
}
|
||||
|
||||
fn open<R: Read + Seek, C: Container>(source: R, dest: &mut C) -> Result<()> {
|
||||
let mut archive = ZipArchive::new(source).map_err(zip_err)?;
|
||||
|
||||
// Zip headers declare each entry's uncompressed size up front; `checked_entry_size` caps the running
|
||||
// total so a malicious archive can't exhaust memory or disk before any bytes are read.
|
||||
let mut total_size = 0u64;
|
||||
|
||||
for index in 0..archive.len() {
|
||||
let mut entry = archive.by_index(index).map_err(zip_err)?;
|
||||
if !entry.is_file() {
|
||||
continue;
|
||||
}
|
||||
let name = entry.name().to_string();
|
||||
validate_path(&name)?;
|
||||
|
||||
let size = checked_entry_size(&mut total_size, entry.size())?;
|
||||
|
||||
dest.write_sized(&name, size, &mut |buffer| {
|
||||
entry.read_exact(buffer).map_err(ContainerError::Io)?;
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write + Seek> ArchiveWriter for ZipWriter<W> {
|
||||
fn write_entry(&mut self, path: &str, bytes: &[u8]) -> Result<()> {
|
||||
validate_path(path)?;
|
||||
self.inner.start_file(path, self.options).map_err(zip_err)?;
|
||||
self.inner.write_all(bytes)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn finish(self) -> Result<()> {
|
||||
self.inner.finish().map_err(zip_err)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write + Seek> ZipWriter<W> {
|
||||
/// Finish the archive and return the underlying sink, for in-memory archives where the caller
|
||||
/// wants the written bytes (e.g. `Cursor<Vec<u8>>`) back.
|
||||
pub fn finish_into(self) -> Result<W> {
|
||||
self.inner.finish().map_err(zip_err)
|
||||
}
|
||||
}
|
||||
|
||||
fn zip_err(error: zip::result::ZipError) -> ContainerError {
|
||||
// Preserve a real I/O failure (disk full, etc.) as a structured `Io` so callers can tell it apart from a
|
||||
// corrupt-archive `Codec` error; only the genuinely archive-format errors collapse into `Codec`.
|
||||
match error {
|
||||
zip::result::ZipError::Io(io) => ContainerError::Io(io),
|
||||
other => ContainerError::Codec(format!("zip: {other}")),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user