From a313eb518c7c57e7066bbfefd9a84ffc4829f19d Mon Sep 17 00:00:00 2001 From: Breadway Date: Tue, 25 Aug 2026 16:14:46 +0800 Subject: [PATCH] bread-theme: validate surfaces.*.anchor like its width/layer siblings anchor was the one enum-shaped schema field taken via unwrap_or_default() with no manifest-time validation, while width and layer both bail! on an unrecognized value (as does bar.window.anchors). A typo'd anchor silently fell through to breadbar's own runtime eprintln + unanchored-window fallback instead of the load-time hard error every other typo'd key gets. Validate against the three shapes breadbar/src/surface.rs actually implements. --- bread-theme/src/shell/manifest.rs | 14 ++++++- bread-theme/src/shell/mod.rs | 61 +++++++++++++++++++++++++++++++ bread-theme/src/shell/types.rs | 7 ++++ 3 files changed, 81 insertions(+), 1 deletion(-) diff --git a/bread-theme/src/shell/manifest.rs b/bread-theme/src/shell/manifest.rs index 724eb25..27620d9 100644 --- a/bread-theme/src/shell/manifest.rs +++ b/bread-theme/src/shell/manifest.rs @@ -414,6 +414,18 @@ fn resolve_surfaces( let mut out = BTreeMap::new(); let Some(raw) = raw else { return Ok(out) }; for (namespace, s) in raw { + let anchor = match s.anchor.as_deref() { + Some(a @ ("top_right" | "bottom_centre" | "fill")) => a.to_string(), + Some(other) => bail!( + "theme '{theme_id}': surfaces.{namespace}.anchor = \"{other}\" is not \ + top_right|bottom_centre|fill (the only shapes breadbar's satellite \ + windows implement — see breadbar/src/surface.rs)" + ), + None => bail!( + "theme '{theme_id}': surfaces.{namespace} has no anchor set \ + (expected one of top_right|bottom_centre|fill)" + ), + }; let offset = match &s.offset { None => vec![], Some(RawOffset::Single(v)) => vec![*v], @@ -439,7 +451,7 @@ fn resolve_surfaces( out.insert( namespace.clone(), Surface { - anchor: s.anchor.clone().unwrap_or_default(), + anchor, offset, width, layer, diff --git a/bread-theme/src/shell/mod.rs b/bread-theme/src/shell/mod.rs index 643d776..94ae0a4 100644 --- a/bread-theme/src/shell/mod.rs +++ b/bread-theme/src/shell/mod.rs @@ -1035,6 +1035,67 @@ mod tests { assert!(format!("{err:#}").contains("hexagon")); } + #[test] + fn unknown_surface_anchor_is_a_hard_error_not_a_silent_default() { + // Before the fix, `surfaces.*.anchor` was the one enum-shaped + // field in the whole schema that skipped manifest-time validation + // entirely (`unwrap_or_default()`), unlike its siblings `width` and + // `layer` which both `bail!`. A typo here must fail exactly like + // any other typo'd enum value. + let xdg = isolated_xdg(); + write_theme( + &xdg, + "badanchor", + r#" + id = "badanchor" + [surfaces."breadbar-notif"] + anchor = "top_lft" + width = 320 + layer = "overlay" + "#, + ); + let err = load_named("badanchor").expect_err("unknown anchor must be a hard error"); + assert!(format!("{err:#}").contains("top_lft")); + } + + #[test] + fn missing_surface_anchor_is_a_hard_error() { + let xdg = isolated_xdg(); + write_theme( + &xdg, + "noanchor", + r#" + id = "noanchor" + [surfaces."breadbar-notif"] + width = 320 + layer = "overlay" + "#, + ); + let err = load_named("noanchor").expect_err("missing anchor must be a hard error"); + assert!(format!("{err:#}").contains("no anchor set")); + } + + #[test] + fn every_known_surface_anchor_shape_resolves() { + for anchor in ["top_right", "bottom_centre", "fill"] { + let xdg = isolated_xdg(); + write_theme( + &xdg, + "okanchor", + &format!( + r#" + id = "okanchor" + [surfaces."breadbar-notif"] + anchor = "{anchor}" + "# + ), + ); + let theme = load_named("okanchor") + .unwrap_or_else(|e| panic!("anchor {anchor} should resolve: {e:#}")); + assert_eq!(theme.surfaces()["breadbar-notif"].anchor, anchor); + } + } + // ---- fallback on broken theme ----------------------------------------- #[test] diff --git a/bread-theme/src/shell/types.rs b/bread-theme/src/shell/types.rs index cf4578c..10f2d36 100644 --- a/bread-theme/src/shell/types.rs +++ b/bread-theme/src/shell/types.rs @@ -200,6 +200,13 @@ pub struct Launcher { /// one lookup. #[derive(Debug, Clone, PartialEq)] pub struct Surface { + /// One of `top_right`/`bottom_centre`/`fill` — validated in + /// `manifest.rs::resolve_surfaces` against the shapes + /// `breadbar/src/surface.rs::apply` actually implements, the same + /// "typo'd key is a hard error" policy this field's siblings + /// (`width`, `layer`) already got. Required, not defaulted: a surface + /// entry with no anchor at all is as much a hard `theme.toml` error as + /// an unrecognized one. pub anchor: String, pub offset: Vec, pub width: SurfaceWidth,