From 9dbcd99e808fa920a6469c4579c935f9a7fcdffb Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Tue, 27 Oct 2026 07:44:59 -0700 Subject: [PATCH] Reject legacy workspace stacks beyond the layout depth limit --- crates/ui-egui/src/workspace_state/legacy.rs | 10 ++++-- .../src/workspace_state/legacy/tests.rs | 35 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 crates/ui-egui/src/workspace_state/legacy/tests.rs diff --git a/crates/ui-egui/src/workspace_state/legacy.rs b/crates/ui-egui/src/workspace_state/legacy.rs index 93d71fd75..bcb5a9691 100644 --- a/crates/ui-egui/src/workspace_state/legacy.rs +++ b/crates/ui-egui/src/workspace_state/legacy.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, 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) -> Result, String> { +fn column(entries: Vec, depth: usize) -> Result, String> { let mut frames: Vec = Vec::new(); // Tabs of closed frames above the first open one. let mut waiting: Vec = Vec::new(); @@ -199,6 +202,9 @@ fn column(entries: Vec) -> Result, 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() { diff --git a/crates/ui-egui/src/workspace_state/legacy/tests.rs b/crates/ui-egui/src/workspace_state/legacy/tests.rs new file mode 100644 index 000000000..a3d182f4f --- /dev/null +++ b/crates/ui-egui/src/workspace_state/legacy/tests.rs @@ -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)); +}