Browse / github.com/storytold/effectcraft / patch
Reject legacy workspace stacks beyond the layout depth limit
Reject legacy workspace stacks beyond the layout depth limit Legacy workspace migration now rejects a stack of open frames before converting it into a split tree deeper than the supported layout limit. The check includes the stack's depth within its parent layout. Regression tests cover the root and nested depth boundaries and a serialized legacy workspace containing 20,000 entries within the existing file-size limit. Both tests failed before the change. All 15 workspace tests pass with the check. All seven Windows checks completed with 4,000 reported test passes, zero failures, and 32 ignored tests. Validation included the changes from #1762, #2283,
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- dotCooCoo (imported from pull request #2515, open)
- Base
01c904b041064ca8ed03e62bffd07f974dee07f6- Size
- +43 −2 in 2 files
- Review
- pending
- Shared
- 2026-10-28
- Id
37b49428d29d9a56739d0ac0
How it went
No other operator has reported using it yet.
Diff
raw · crates/ui-egui/src/workspace_state/legacy.rs crates/ui-egui/src/workspace_state/legacy/tests.rs
@@ -25,6 +25,9 @@ use super::{Snapshot, VERSION}; use crate::dock::{Layout, PanelKind, Workspace}; use crate::state::UiState; +#[cfg(test)]+mod tests;+ #[derive(Deserialize)] enum OldSplitSize { Ratio(f32),@@ -166,7 +169,7 @@ fn node(old: OldNode, depth: usize) -> Result<Node<PanelKind>, String> { first: Box::new(node(*a, depth.saturating_add(1))?), second: Box::new(node(*b, depth.saturating_add(1))?), },- OldNode::Stack { entries } => column(entries)?,+ OldNode::Stack { entries } => column(entries, depth)?, }) } @@ -183,7 +186,7 @@ struct Frame { } /// A column of stacked frames as tab groups split top to bottom (see the module docs).-fn column(entries: Vec<OldStackEntry>) -> Result<Node<PanelKind>, String> {+fn column(entries: Vec<OldStackEntry>, depth: usize) -> Result<Node<PanelKind>, String> { let mut frames: Vec<Frame> = Vec::new(); // Tabs of closed frames above the first open one. let mut waiting: Vec<PanelKind> = Vec::new();@@ -199,6 +202,9 @@ fn column(entries: Vec<OldStackEntry>) -> Result<Node<PanelKind>, String> { } let active = entry.active.min(panels.len().saturating_sub(1)); if entry.open {+ if depth.saturating_add(frames.len()) > craft_ui::docking::MAX_DEPTH {+ return Err("workspace dock tree exceeds supported limits".into());+ } let height = entry.height.filter(|h| h.is_finite() && *h > 0.0).map(|h| h.min(MAX_FRAME)); frames.push(Frame { panels, active, height }); } else if let Some(above) = frames.last_mut() {new file mode 100644@@ -0,0 +1,35 @@+use super::*;++fn stack(count: usize) -> OldNode {+ OldNode::Stack {+ entries: (0..count).map(|_| OldStackEntry { panel: Some(PanelKind::Info), panels: Vec::new(), active: 0, open: true, height: None }).collect(),+ }+}++#[test]+fn stacked_frames_respect_the_remaining_tree_depth() {+ let limit = craft_ui::docking::MAX_DEPTH;+ assert!(node(stack(limit + 1), 0).is_ok());+ assert!(node(stack(limit + 2), 0).is_err());+ assert!(node(stack(1), limit).is_ok());+ assert!(node(stack(2), limit).is_err());+ assert!(node(stack(limit), 2).is_err());+}++#[test]+fn oversized_legacy_stack_load_does_not_abort() {+ const CHILD: &str = "EFFECTCRAFT_TEST_LEGACY_STACK_CHILD";+ if std::env::var_os(CHILD).is_some() {+ let entry = serde_json::json!({"panel": "Info", "open": true});+ let value = serde_json::json!({"workspaces": {"Oversized": {"Stack": {"entries": vec![entry; 20_000]}}}});+ assert!(serde_json::to_vec(&value).unwrap().len() < super::super::MAX_BYTES);+ assert!(migrate(value, &UiState::default()).is_err());+ return;+ }+ let output = std::process::Command::new(std::env::current_exe().unwrap())+ .args(["--exact", "workspace_state::legacy::tests::oversized_legacy_stack_load_does_not_abort", "--nocapture"])+ .env(CHILD, "1")+ .output()+ .unwrap();+ assert!(output.status.success(), "{}\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr));+}