Skip to content
Merged
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
1 change: 1 addition & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions crates/octoscript-ui-l0/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ license = "Apache-2.0"
# No VM dependency: hosts can use the profile across Makepad lineages.
[dependencies]
serde_json = "1"
serde = { version = "1", features = ["derive"] }
blake3 = "1.8"

[dev-dependencies]
Expand Down
70 changes: 69 additions & 1 deletion crates/octoscript-ui-l0/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9385,7 +9385,7 @@ pub const CARD_STATE_KEY: &str = "@card";
/// Nothing in the store is authored by the model. A card cannot name an
/// instance, cannot address another instance's cell, and cannot write anything
/// except through a declared transition on its own state.
#[derive(Clone, Debug, Default)]
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct InstanceStore {
/// `instance-key` → field → value.
cells: BTreeMap<String, BTreeMap<String, (serde_json::Value, ValueOrigin)>>,
Expand All @@ -9398,6 +9398,74 @@ pub struct InstanceStore {
}

impl InstanceStore {
/// Versioned, bounded host snapshot. The host must keep these bytes in a
/// host-owned directory: origins are trusted metadata, not app input.
pub fn snapshot_bytes(&self) -> Result<Vec<u8>, String> {
self.check_snapshot_limits()?;
let bytes = serde_json::to_vec(&serde_json::json!({"schema": 1, "state": self}))
.map_err(|e| e.to_string())?;
if bytes.len() > 1_048_576 {
return Err("Card state snapshot exceeds 1 MiB".into());
}
Ok(bytes)
}

/// Read only snapshots written by the host. The next realization still
/// reconciles component schema and prunes cells that are no longer live.
pub fn from_snapshot_bytes(bytes: &[u8]) -> Result<Self, String> {
if bytes.len() > 1_048_576 {
return Err("Card state snapshot exceeds 1 MiB".into());
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct Snapshot {
schema: u32,
state: InstanceStore,
}
let snapshot: Snapshot = serde_json::from_slice(bytes).map_err(|e| e.to_string())?;
if snapshot.schema != 1 {
return Err("unsupported Card state snapshot schema".into());
}
snapshot.state.check_snapshot_limits()?;
Ok(snapshot.state)
}

fn check_snapshot_limits(&self) -> Result<(), String> {
if self.cells.len() > 1024 || self.schemas.len() > 128 || self.shapes.len() > 128 {
return Err("Card state snapshot has too many instances or schemas".into());
}
let mut fields = 0usize;
let mut value_bytes = 0usize;
for (key, values) in &self.cells {
if key.len() > 256 {
return Err("Card state instance key is too long".into());
}
fields = fields.saturating_add(values.len());
if fields > 8192 || values.keys().any(|field| field.len() > 256) {
return Err("Card state snapshot has too many or oversized fields".into());
}
for (value, _) in values.values() {
value_bytes = value_bytes
.saturating_add(serde_json::to_vec(value).map_err(|e| e.to_string())?.len());
if value_bytes > 1_048_576 {
return Err("Card state snapshot exceeds 1 MiB".into());
}
}
}
if self.schemas.keys().any(|name| name.len() > 256)
|| self.shapes.iter().any(|(name, paths)| {
name.len() > 256
|| paths.len() > 8192
|| paths
.iter()
.any(|(path, shape)| path.len() > 256 || shape.len() > 4096)
})
{
return Err("Card state snapshot has oversized schemas".into());
}
Ok(())
}

pub fn get(&self, key: &str, field: &str) -> Option<&serde_json::Value> {
self.cells.get(key)?.get(field).map(|(v, _)| v)
}
Expand Down
3 changes: 2 additions & 1 deletion crates/octoscript-ui-l0/src/value_origin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@

use crate::*;

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ValueOrigin {
#[default]
Authored,
Expand Down
16 changes: 16 additions & 0 deletions crates/octoscript-ui-l0/tests/state_snapshot.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use octoscript_ui_l0::{InstanceStore, ValueOrigin};

#[test]
fn snapshot_roundtrip_keeps_cells_and_origins() {
let mut state = InstanceStore::default();
state.set_cell_with_origin("@card", "note", "hello".into(), ValueOrigin::UserInput);
let bytes = state.snapshot_bytes().unwrap();
let restored = InstanceStore::from_snapshot_bytes(&bytes).unwrap();
assert_eq!(restored.get("@card", "note").unwrap(), "hello");
assert_eq!(
restored.origin("@card", "note"),
Some(ValueOrigin::UserInput)
);
assert!(InstanceStore::from_snapshot_bytes(b"{\"schema\":99}").is_err());
assert!(InstanceStore::from_snapshot_bytes(&vec![b' '; 1_048_577]).is_err());
}
1 change: 1 addition & 0 deletions fuzz/Cargo.lock

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

Loading