Files
fluxer/fluxer_desktop/native/linux-portals/src/gnome_shell.rs
T

154 lines
4.7 KiB
Rust

// SPDX-License-Identifier: AGPL-3.0-or-later
#[cfg(target_os = "linux")]
use std::time::Duration;
#[cfg(target_os = "linux")]
pub const GNOME_SHELL_DESTINATION: &str = "org.gnome.Shell";
#[cfg(target_os = "linux")]
pub const GNOME_SHELL_PATH: &str = "/org/gnome/Shell";
#[cfg(target_os = "linux")]
pub const GNOME_SHELL_INTERFACE: &str = "org.gnome.Shell";
#[cfg(target_os = "linux")]
pub const GNOME_SHELL_EVAL_TIMEOUT: Duration = Duration::from_millis(1_500);
pub fn is_safe_shell_eval_token(token: &str) -> bool {
if token.is_empty() || token.len() > 128 {
return false;
}
token
.bytes()
.all(|ch| ch.is_ascii_alphanumeric() || ch == b'_')
}
pub fn is_gnome_eval_disabled_via_env() -> bool {
match std::env::var("FLUXER_PORTALS_GNOME_EVAL") {
Ok(v) => {
let lower = v.to_ascii_lowercase();
lower == "0" || lower == "false" || lower == "no"
}
Err(_) => false,
}
}
pub fn parse_shell_eval_pid_payload(payload: &str) -> Option<u32> {
let trimmed = payload.trim();
if trimmed.is_empty() {
return None;
}
let stripped = trimmed
.trim_start_matches(['[', ' ', '\t'])
.trim_end_matches([']', ' ', '\t', ','])
.trim();
if stripped.is_empty() {
return None;
}
let mut end = 0;
for (i, ch) in stripped.char_indices() {
if ch.is_ascii_digit() {
end = i + ch.len_utf8();
} else {
break;
}
}
if end == 0 {
return None;
}
let number: u64 = stripped[..end].parse().ok()?;
if number == 0 || number > u32::MAX as u64 {
return None;
}
Some(number as u32)
}
pub fn build_window_pid_script(token: &str) -> String {
format!(
"global.get_window_actors().map(a=>a.meta_window).filter(w=>w.get_id&&w.get_id().toString()===\"{token}\").map(w=>w.get_pid())[0]"
)
}
#[cfg(target_os = "linux")]
pub fn resolve_gnome_shell_window_pid(token: &str) -> Result<Option<u32>, String> {
if is_gnome_eval_disabled_via_env() {
return Ok(None);
}
if !is_safe_shell_eval_token(token) {
return Err("resolveWindowPid: token failed validation".into());
}
let conn = zbus::blocking::connection::Builder::session()
.map_err(|err| format!("openSessionBus failed: {err}"))?
.method_timeout(GNOME_SHELL_EVAL_TIMEOUT)
.build()
.map_err(|err| format!("openSessionBus failed: {err}"))?;
let proxy = zbus::blocking::Proxy::new(
&conn,
GNOME_SHELL_DESTINATION,
GNOME_SHELL_PATH,
GNOME_SHELL_INTERFACE,
)
.map_err(|err| format!("shell Eval failed: {err}"))?;
let script = build_window_pid_script(token);
let (success, payload): (bool, String) = proxy
.call("Eval", &(script.as_str(),))
.map_err(|err| format!("shell Eval failed: {err}"))?;
if !success {
return Ok(None);
}
Ok(parse_shell_eval_pid_payload(&payload))
}
#[cfg(not(target_os = "linux"))]
pub fn resolve_gnome_shell_window_pid(_token: &str) -> Result<Option<u32>, String> {
Err("not supported on this platform".into())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_safe_shell_eval_token_accepts_simple() {
assert!(is_safe_shell_eval_token("abc"));
assert!(is_safe_shell_eval_token("Window_123"));
}
#[test]
fn is_safe_shell_eval_token_rejects_metacharacters() {
assert!(!is_safe_shell_eval_token(""));
assert!(!is_safe_shell_eval_token("\"; system('rm -rf'); \""));
assert!(!is_safe_shell_eval_token("abc def"));
assert!(!is_safe_shell_eval_token("abc-def"));
}
#[test]
fn is_safe_shell_eval_token_rejects_overlong() {
assert!(!is_safe_shell_eval_token(&"a".repeat(129)));
assert!(is_safe_shell_eval_token(&"a".repeat(128)));
}
#[test]
fn parse_shell_eval_pid_payload_plain_integer() {
assert_eq!(parse_shell_eval_pid_payload("12345"), Some(12345));
}
#[test]
fn parse_shell_eval_pid_payload_array_wrapped() {
assert_eq!(parse_shell_eval_pid_payload("[12345]"), Some(12345));
assert_eq!(parse_shell_eval_pid_payload("[ 12345 ]"), Some(12345));
}
#[test]
fn parse_shell_eval_pid_payload_rejects_garbage() {
assert_eq!(parse_shell_eval_pid_payload(""), None);
assert_eq!(parse_shell_eval_pid_payload("undefined"), None);
assert_eq!(parse_shell_eval_pid_payload("0"), None);
}
#[test]
fn build_window_pid_script_splices_token() {
let s = build_window_pid_script("Window_42");
assert!(s.contains("===\"Window_42\""));
assert!(s.contains("global.get_window_actors()"));
}
}