mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-16 23:08:05 +08:00
* Add document-format crate * Adapt document-format to merged graph-storage and consolidate errors * Fix resource write ordering, codec error masking, and export docs * Remove document-format RFC; tracked in a dedicated PR * Restructure document-format into gdd/persist/export/resource modules Split the 963-line lib.rs by responsibility: persist path into persist.rs, resource I/O into resource.rs, export engine into export.rs. Rename the GddV1 layout struct to GddV1Layout and add a GddV1 = Gdd<GddV1Layout> alias. * Feature-split document-format and graph-storage for minimal builds * Address PR review: O(1) history-delta lookup and no-default-features test build * Address PR review: propagate apply_hot_op persist error and assert resource hash * Make decompression from archive streaming * Share archive export body between export and export_to_bytes * Review cleanup * Export documents with un-retired hot ops losslessly * Persist last_broadcast_rev in session.json, drop unused last_retired_at * Move peer_id from manifest to session.json * Rename gesture to interaction in document-format storage layer
77 lines
2.2 KiB
Rust
77 lines
2.2 KiB
Rust
//! Zip archive codec.
|
|
|
|
use crate::archive::{Archive, ArchiveWriter, checked_entry_size};
|
|
use crate::{AsyncContainer, 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: AsyncContainer>(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_non_blocking(&name, size, &mut |buffer| {
|
|
entry.read_exact(buffer).map_err(ContainerError::Io)?;
|
|
Ok(())
|
|
})?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl<W: Write + Seek> ArchiveWriter for ZipWriter<W> {
|
|
type Sink = 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_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}")),
|
|
}
|
|
}
|