mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 21:45:31 +09:00
154 lines
4.7 KiB
Rust
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()"));
|
|
}
|
|
}
|