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
|
|
@ -24,12 +24,15 @@ pub const KNOWN_APPS: &[&str] = &[
|
|||
/// socket client may freely emit a custom/test event, but not one whose
|
||||
/// top-level segment is one of these, since that would let it impersonate
|
||||
/// a real adapter (or another daemon-internal event family) rather than
|
||||
/// producing an obviously-manual one. See [`is_reserved_domain`] and
|
||||
/// `breadd/src/ipc/mod.rs`'s `emit` handler. *Since: v1.5 — `bluetooth`,
|
||||
/// producing an obviously-manual one. The one exception is a well-formed
|
||||
/// [`validate_command_event`] name (`bread.command.<known-app>.<verb>`):
|
||||
/// `command` stays reserved so it cannot be claimed as an app id, but the
|
||||
/// command bus itself is meant to be publishable. See [`is_reserved_domain`]
|
||||
/// and `breadd/src/ipc/mod.rs`'s `emit` handler. *Since: v1.5 — `bluetooth`,
|
||||
/// `workspace`, `window`, and `monitor` added (event families the Hyprland
|
||||
/// and Bluetooth adapters already published under, but that were missing
|
||||
/// from this list) when this became a spoofing-prevention boundary and not
|
||||
/// just an app-id-conflict one.*
|
||||
/// just an app-id-conflict one. Since: v1.7 — command-bus exception.*
|
||||
const RESERVED_DOMAINS: &[&str] = &[
|
||||
"terminal",
|
||||
"git",
|
||||
|
|
@ -66,11 +69,54 @@ pub fn is_reserved_domain(id: &str) -> bool {
|
|||
/// Whether `event` is a well-formed event name for `app` — i.e. it starts
|
||||
/// with `bread.<app>.`. An app may only publish within its own namespace
|
||||
/// segment; this is what the IPC boundary checks before constructing a
|
||||
/// `RawEvent` tagged `AdapterSource::App(app)`.
|
||||
/// `RawEvent` tagged `AdapterSource::App(app)`. Command events addressed
|
||||
/// to another known app are a separate, explicit exception — see
|
||||
/// [`validate_command_event`].
|
||||
pub fn validate_app_namespace(app: &str, event: &str) -> bool {
|
||||
event.starts_with(&format!("bread.{app}."))
|
||||
}
|
||||
|
||||
/// Whether `event` is in the outbound command namespace
|
||||
/// (`bread.command.*`). Does not check that the target is a known app —
|
||||
/// use [`validate_command_event`] for that.
|
||||
pub fn is_command_event(event: &str) -> bool {
|
||||
event.starts_with("bread.command.")
|
||||
}
|
||||
|
||||
/// The app id a command event is addressed to — the segment immediately
|
||||
/// after `bread.command.`. Returns `None` if `event` is not a command
|
||||
/// event or the app-id segment is empty.
|
||||
pub fn command_target(event: &str) -> Option<&str> {
|
||||
let rest = event.strip_prefix("bread.command.")?;
|
||||
let app = rest.split('.').next()?;
|
||||
if app.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(app)
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `event` is a well-formed command to a registered sibling app:
|
||||
/// `bread.command.<known_app>.<verb>` with `known_app` in [`KNOWN_APPS`]
|
||||
/// and a non-empty verb (at least one extra dotted segment).
|
||||
///
|
||||
/// This is the exception the IPC unsourced/`bread-emit` path (and sourced
|
||||
/// `AdapterSource::App` emit) use so any module or app can publish
|
||||
/// commands without `command` leaving [`is_reserved_domain`] — `command`
|
||||
/// must stay unclaimable as an app id. `bread.command.power.off` and
|
||||
/// `bread.command.notanapp.x` still fail because the target is not in
|
||||
/// [`KNOWN_APPS`].
|
||||
pub fn validate_command_event(event: &str) -> bool {
|
||||
let rest = match event.strip_prefix("bread.command.") {
|
||||
Some(rest) => rest,
|
||||
None => return false,
|
||||
};
|
||||
let Some((app, verb)) = rest.split_once('.') else {
|
||||
return false;
|
||||
};
|
||||
is_known_app(app) && !verb.is_empty()
|
||||
}
|
||||
|
||||
/// The top-level dotted segment after `bread.` in an event name — e.g.
|
||||
/// `Some("power")` for `"bread.power.ac.connected"`. Returns `None` for
|
||||
/// event names that don't start with `bread.` at all, which are always
|
||||
|
|
@ -119,8 +165,15 @@ mod tests {
|
|||
// daemon itself publishes under must be reserved, or a manual/no-source
|
||||
// `emit` over the IPC socket could impersonate it undetected.
|
||||
for domain in [
|
||||
"power", "network", "device", "bluetooth", "hyprland", "workspace", "monitor",
|
||||
"window", "system",
|
||||
"power",
|
||||
"network",
|
||||
"device",
|
||||
"bluetooth",
|
||||
"hyprland",
|
||||
"workspace",
|
||||
"monitor",
|
||||
"window",
|
||||
"system",
|
||||
] {
|
||||
assert!(
|
||||
is_reserved_domain(domain),
|
||||
|
|
@ -164,4 +217,47 @@ mod tests {
|
|||
// because it shares a string prefix.
|
||||
assert!(!validate_app_namespace("clip", "bread.clipx.copied"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_command_event_requires_command_prefix() {
|
||||
assert!(is_command_event("bread.command.clip.clear"));
|
||||
assert!(is_command_event("bread.command.power.off"));
|
||||
assert!(!is_command_event("bread.command"));
|
||||
assert!(!is_command_event("bread.clip.copied"));
|
||||
assert!(!is_command_event("command.clip.clear"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_target_extracts_app_id() {
|
||||
assert_eq!(command_target("bread.command.clip.clear"), Some("clip"));
|
||||
assert_eq!(command_target("bread.command.cast.start.now"), Some("cast"));
|
||||
assert_eq!(command_target("bread.command.clip"), Some("clip"));
|
||||
assert_eq!(command_target("bread.command."), None);
|
||||
assert_eq!(command_target("bread.clip.copied"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_command_event_accepts_known_app_with_verb() {
|
||||
assert!(validate_command_event("bread.command.clip.clear"));
|
||||
assert!(validate_command_event("bread.command.cast.start"));
|
||||
assert!(validate_command_event("bread.command.clip.stack.clear"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_command_event_rejects_unknown_target_or_missing_verb() {
|
||||
assert!(!validate_command_event("bread.command.power.off"));
|
||||
assert!(!validate_command_event("bread.command.notanapp.x"));
|
||||
assert!(!validate_command_event("bread.command.clip"));
|
||||
assert!(!validate_command_event("bread.command.clip."));
|
||||
assert!(!validate_command_event("bread.command."));
|
||||
assert!(!validate_command_event("bread.hyprland.workspace.changed"));
|
||||
assert!(!validate_command_event("bread.clip.copied"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_stays_reserved_and_is_not_a_known_app() {
|
||||
assert!(is_reserved_domain("command"));
|
||||
assert!(!is_known_app("command"));
|
||||
assert!(!validate_command_event("bread.command.command.x"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue