Allow well-formed bread.command events on the emit bus
Docs already said any module or app could publish bread.command.<app>.<verb>, but command is reserved so both unsourced bread-emit and sourced App emit rejected the whole namespace. Keep command unclaimable as an app id; accept bread.command.<known-app>.<verb> (and let an app command another known app). Also ship bread-emit and bread-module-host, and give udev enumerate the same classification fields as a live add so boot-time devices are not all unknown.
This commit is contained in:
parent
a6973360bd
commit
d3517d1433
52 changed files with 26405 additions and 6811 deletions
|
|
@ -1,7 +1,10 @@
|
|||
use std::collections::HashMap;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use bread_shared::{apps::validate_app_namespace, AdapterSource, BreadEvent, RawEvent};
|
||||
use bread_shared::{
|
||||
apps::{validate_app_namespace, validate_command_event},
|
||||
AdapterSource, BreadEvent, RawEvent,
|
||||
};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
/// How many multiples of `dedup_window_ms` an entry must be idle before eviction.
|
||||
|
|
@ -208,9 +211,11 @@ impl EventNormalizer {
|
|||
"workspace" | "workspacev2" => {
|
||||
self.emit_hyprland_dual("bread.workspace.changed", raw.payload.clone(), raw)
|
||||
}
|
||||
"createworkspace" => {
|
||||
self.emit_hyprland_dual("bread.workspace.created", json!({ "workspace": data }), raw)
|
||||
}
|
||||
"createworkspace" => self.emit_hyprland_dual(
|
||||
"bread.workspace.created",
|
||||
json!({ "workspace": data }),
|
||||
raw,
|
||||
),
|
||||
"destroyworkspace" => self.emit_hyprland_dual(
|
||||
"bread.workspace.destroyed",
|
||||
json!({ "workspace": data }),
|
||||
|
|
@ -589,7 +594,10 @@ impl EventNormalizer {
|
|||
let AdapterSource::App(app) = &raw.source else {
|
||||
return vec![];
|
||||
};
|
||||
if !validate_app_namespace(app, &raw.kind) {
|
||||
// Own-namespace events plus well-formed commands to another known
|
||||
// app (the command bus). Anything else — including spoofed adapter
|
||||
// namespaces — is dropped here even if it somehow crossed IPC.
|
||||
if !validate_app_namespace(app, &raw.kind) && !validate_command_event(&raw.kind) {
|
||||
return vec![];
|
||||
}
|
||||
vec![BreadEvent {
|
||||
|
|
@ -997,7 +1005,11 @@ mod tests {
|
|||
1,
|
||||
);
|
||||
let out = n.normalize(&ev);
|
||||
assert_eq!(out.len(), 2, "kind {kind} should dual-emit exactly 2 events");
|
||||
assert_eq!(
|
||||
out.len(),
|
||||
2,
|
||||
"kind {kind} should dual-emit exactly 2 events"
|
||||
);
|
||||
assert!(
|
||||
out.iter().any(|e| &e.event == legacy_event),
|
||||
"kind {kind} missing legacy event {legacy_event}"
|
||||
|
|
@ -1006,7 +1018,12 @@ mod tests {
|
|||
out.iter().any(|e| &e.event == namespaced_event),
|
||||
"kind {kind} missing namespaced event {namespaced_event}"
|
||||
);
|
||||
let legacy_data = out.iter().find(|e| &e.event == legacy_event).unwrap().data.clone();
|
||||
let legacy_data = out
|
||||
.iter()
|
||||
.find(|e| &e.event == legacy_event)
|
||||
.unwrap()
|
||||
.data
|
||||
.clone();
|
||||
let namespaced_data = out
|
||||
.iter()
|
||||
.find(|e| &e.event == namespaced_event)
|
||||
|
|
@ -1452,6 +1469,65 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
// ─── App / command bus ─────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn app_own_namespace_passes_through() {
|
||||
let n = EventNormalizer::new(0);
|
||||
let out = n.normalize(&raw(
|
||||
AdapterSource::App("clip".into()),
|
||||
"bread.clip.copied",
|
||||
json!({"len": 4}),
|
||||
1,
|
||||
));
|
||||
assert_eq!(out.len(), 1);
|
||||
assert_eq!(out[0].event, "bread.clip.copied");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_command_to_another_known_app_passes_through() {
|
||||
let n = EventNormalizer::new(0);
|
||||
let out = n.normalize(&raw(
|
||||
AdapterSource::App("cast".into()),
|
||||
"bread.command.clip.clear",
|
||||
json!({}),
|
||||
1,
|
||||
));
|
||||
assert_eq!(out.len(), 1);
|
||||
assert_eq!(out[0].event, "bread.command.clip.clear");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_wrong_namespace_is_dropped() {
|
||||
let n = EventNormalizer::new(0);
|
||||
let out = n.normalize(&raw(
|
||||
AdapterSource::App("cast".into()),
|
||||
"bread.clip.copied",
|
||||
json!({}),
|
||||
1,
|
||||
));
|
||||
assert!(out.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_command_to_unknown_or_reserved_target_is_dropped() {
|
||||
let n = EventNormalizer::new(0);
|
||||
let power = n.normalize(&raw(
|
||||
AdapterSource::App("cast".into()),
|
||||
"bread.command.power.off",
|
||||
json!({}),
|
||||
1,
|
||||
));
|
||||
assert!(power.is_empty());
|
||||
let spoof = n.normalize(&raw(
|
||||
AdapterSource::App("cast".into()),
|
||||
"bread.hyprland.workspace.changed",
|
||||
json!({}),
|
||||
1,
|
||||
));
|
||||
assert!(spoof.is_empty());
|
||||
}
|
||||
|
||||
// ─── Helper ────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue