Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ members = [
"desktop/platform/win",
"document/container",
"document/graph-storage",
"document/document-format",
"editor",
"frontend/wrapper",
"libraries/dyn-any",
Expand Down Expand Up @@ -88,7 +89,9 @@ repeat-nodes = { path = "node-graph/nodes/repeat" }
math-nodes = { path = "node-graph/nodes/math" }
path-bool-nodes = { path = "node-graph/nodes/path-bool" }
graph-craft = { path = "node-graph/graph-craft" }
graph-storage = { path = "document/graph-storage" }
graph-storage = { path = "document/graph-storage", default-features = false }
document-format = { path = "document/document-format" }
document-container = { path = "document/container" }
raster-nodes = { path = "node-graph/nodes/raster" }
graphene-std = { path = "node-graph/nodes/gstd" }
interpreted-executor = { path = "node-graph/interpreted-executor" }
Expand Down
16 changes: 11 additions & 5 deletions document/container/src/archive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
//! Each codec streams entries in both directions: writers wrap an `io::Write` sink, and
//! `deserialize` reads from any `io::Read + Seek` source and streams entries into any [`Container`].

#[cfg(any(feature = "xz", feature = "zip"))]
use crate::AsyncContainer;
#[cfg(any(feature = "zip", feature = "xz"))]
use crate::ContainerError;
use crate::{Container, Result};
use crate::Result;
use std::io::{Read, Seek, Write};

/// Hard cap on the total decompressed size a codec will materialize from one archive.
Expand Down Expand Up @@ -55,7 +57,7 @@ pub trait Archive {

/// Read entries from `source` and write each into `dest`, streaming so neither the full
/// archive nor the full container ever sits in memory at once.
fn open<R: Read + Seek, C: Container>(source: R, dest: &mut C) -> Result<()>;
fn open<R: Read + Seek, C: AsyncContainer>(source: R, dest: &mut C) -> Result<()>;
}

pub trait ArchiveWriter {
Expand Down Expand Up @@ -86,12 +88,16 @@ impl ArchiveFormat {

/// Deserialize an archive into `dest`, auto-detecting the format from `bytes`' magic header.
/// Errors if the bytes are neither a recognized xz nor zip archive.
#[cfg(all(feature = "xz", feature = "zip"))]
pub fn open_auto<C: Container>(bytes: &[u8], dest: &mut C) -> Result<()> {
#[cfg(any(feature = "xz", feature = "zip"))]
pub fn open_auto<C: AsyncContainer>(bytes: &[u8], dest: &mut C) -> Result<()> {
let source = std::io::Cursor::new(bytes);
match ArchiveFormat::detect(bytes) {
#[cfg(feature = "xz")]
Some(ArchiveFormat::Xz) => Xz::open(source, dest),
#[cfg(feature = "zip")]
Some(ArchiveFormat::Zip) => Zip::open(source, dest),
None => Err(ContainerError::Codec("unrecognized archive format (not xz or zip)".into())),
#[allow(unreachable_patterns)]
Some(format) => Err(ContainerError::Codec(format!("tried to open {format:?}, but the binary was compiled without the feature enabled "))),
None => Err(ContainerError::Codec("unrecognized archive format (not xz or zip) ".into())),
}
}
6 changes: 3 additions & 3 deletions document/container/src/archive/xz.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Xz-compressed tarball archive codec.

use crate::archive::{Archive, ArchiveWriter, MAX_DECOMPRESSED_SIZE, checked_entry_size};
use crate::{Container, ContainerError, Result, validate_path};
use crate::{AsyncContainer, ContainerError, Result, validate_path};
use lzma_rust2::{XzOptions, XzReader, XzWriter as InnerXzWriter};
use std::io::{Read, Seek, Write};

Expand All @@ -23,7 +23,7 @@ impl Archive for Xz {
})
}

fn open<R: Read + Seek, C: Container>(source: R, dest: &mut C) -> Result<()> {
fn open<R: Read + Seek, C: AsyncContainer>(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.
Expand All @@ -46,7 +46,7 @@ impl Archive for Xz {

let size = checked_entry_size(&mut total_size, entry.size())?;

dest.write_sized(&path, size, &mut |buffer| {
dest.write_sized_non_blocking(&path, size, &mut |buffer| {
entry.read_exact(buffer).map_err(ContainerError::Io)?;
Ok(())
})?;
Expand Down
6 changes: 3 additions & 3 deletions document/container/src/archive/zip.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Zip archive codec.

use crate::archive::{Archive, ArchiveWriter, checked_entry_size};
use crate::{Container, ContainerError, Result, validate_path};
use crate::{AsyncContainer, ContainerError, Result, validate_path};
use std::io::{Read, Seek, Write};

use zip::ZipArchive;
Expand All @@ -24,7 +24,7 @@ impl Archive for Zip {
})
}

fn open<R: Read + Seek, C: Container>(source: R, dest: &mut C) -> Result<()> {
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
Expand All @@ -41,7 +41,7 @@ impl Archive for Zip {

let size = checked_entry_size(&mut total_size, entry.size())?;

dest.write_sized(&name, size, &mut |buffer| {
dest.write_sized_non_blocking(&name, size, &mut |buffer| {
entry.read_exact(buffer).map_err(ContainerError::Io)?;
Ok(())
})?;
Expand Down
12 changes: 12 additions & 0 deletions document/container/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,14 @@ pub trait AsyncContainer {
/// and `Ok` is returned eagerly; a later failure is logged.
fn write_non_blocking(&self, path: &str, bytes: &[u8]) -> Result<()>;

/// Synchronous write. Same eager-enqueue semantics on OPFS as [`write_non_blocking`](Self::write_non_blocking);
/// queued appends preserve order relative to earlier queued writes/appends.
fn write_sized_non_blocking(&self, path: &str, size: usize, fill: &mut dyn FnMut(&mut [u8]) -> Result<()>) -> Result<()> {
let mut buf = vec![0u8; size];
fill(&mut buf)?;
self.write_non_blocking(path, &buf)
}

/// Synchronous append. Same eager-enqueue semantics on OPFS as [`write_non_blocking`](Self::write_non_blocking);
/// queued appends preserve order relative to earlier queued writes/appends.
fn append_non_blocking(&self, path: &str, bytes: &[u8]) -> Result<()>;
Expand Down Expand Up @@ -320,6 +328,10 @@ impl<C: Container + ?Sized> AsyncContainer for C {
Container::write(self, path, bytes)
}

fn write_sized_non_blocking(&self, path: &str, size: usize, fill: &mut dyn FnMut(&mut [u8]) -> Result<()>) -> Result<()> {
Container::write_sized(self, path, size, fill)
}

fn append_non_blocking(&self, path: &str, bytes: &[u8]) -> Result<()> {
Container::append(self, path, bytes)
}
Expand Down
37 changes: 37 additions & 0 deletions document/document-format/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
[package]
name = "document-format"
description = "Typed handle for the .gdd document format, sitting over graph-storage and document-container"
edition.workspace = true
version.workspace = true
license.workspace = true
authors.workspace = true

[features]
# Runtime bridge: the editor↔storage conversion methods (stage/commit from a `NodeNetwork`,
# `network_ids`, `declarations`). Off lets a standalone migration tool build without `graph-craft`
# or `core-types`. Forwards to `graph-storage/conversion`.
conversion = ["graph-storage/conversion", "dep:graph-craft", "dep:core-types"]
# Compressed-archive export/open. Each forwards to the matching `document-container` feature and
# gates that format's `ExportFormat` arm and sink. The folder/memory codec-free core needs neither.
zip = ["document-container/zip"]
xz = ["document-container/xz"]
default = ["conversion", "zip", "xz"]

[dependencies]
document-container = { workspace = true }
graph-storage = { workspace = true, default-features = false }
graph-craft = { workspace = true, optional = true }
graphene-resource = { workspace = true }
core-types = { workspace = true, optional = true }
serde = { workspace = true }
serde_json = { workspace = true }
rmp-serde = { workspace = true }
futures = { workspace = true }
chrono = { workspace = true }
thiserror = "2.0"
log = { workspace = true }

[dev-dependencies]
futures = { workspace = true }
graphene-resource = { workspace = true }
tempfile = "3"
Loading
Loading