use crate::error::ServiceError;
pub fn parse_linger_output(output: &str) -> Result<bool, ServiceError> {
let trimmed = output.trim();
if let Some(value) = trimmed.strip_prefix("Linger=") {
match value.trim() {
"yes" => Ok(true),
"no" => Ok(false),
other => Err(ServiceError::Platform(format!(
"Unexpected Linger value: {other}"
))),
}
} else {
Err(ServiceError::Platform(format!(
"Unexpected loginctl output format: {trimmed}"
)))
}
}
pub fn parse_entitlements_for_sandbox(xml: &str) -> bool {
let key = "com.apple.security.app-sandbox";
let Some(pos) = xml.find(key) else {
return false;
};
let after = &xml[pos + key.len()..];
let Some(key_end) = after.find("</key>") else {
return false;
};
let value_section = &after[key_end + "</key>".len()..];
value_section.trim_start().starts_with("<true/>")
}
#[cfg(target_os = "linux")]
pub fn check_systemd_lingering() -> Result<bool, ServiceError> {
let username = std::env::var("USER")
.map_err(|_| ServiceError::Platform("USER environment variable not set".into()))?;
let output = std::process::Command::new("loginctl")
.args(["show-user", &username, "-p", "Linger"])
.output()
.map_err(|e| ServiceError::Platform(format!("Failed to run loginctl: {e}")))?;
if !output.status.success() {
return Err(ServiceError::Platform(format!(
"loginctl exited with status {}: {}",
output.status,
String::from_utf8_lossy(&output.stderr).trim()
)));
}
parse_linger_output(&String::from_utf8_lossy(&output.stdout))
}
#[cfg(target_os = "macos")]
pub fn check_macos_sandbox() -> Result<bool, ServiceError> {
let exe_path = std::env::current_exe()
.map_err(|e| ServiceError::Platform(format!("Cannot determine executable path: {e}")))?;
let output = std::process::Command::new("codesign")
.args(["-d", "--entitlements", ":-", &exe_path.to_string_lossy()])
.output()
.map_err(|e| ServiceError::Platform(format!("Failed to run codesign: {e}")))?;
if !output.status.success() {
return Ok(false);
}
Ok(parse_entitlements_for_sandbox(&String::from_utf8_lossy(
&output.stdout,
)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn linger_yes() {
assert_eq!(parse_linger_output("Linger=yes").unwrap(), true);
}
#[test]
fn linger_no() {
assert_eq!(parse_linger_output("Linger=no").unwrap(), false);
}
#[test]
fn linger_with_trailing_newline() {
assert_eq!(parse_linger_output("Linger=yes\n").unwrap(), true);
}
#[test]
fn linger_unexpected_value() {
let result = parse_linger_output("Linger=maybe");
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(msg.contains("Unexpected Linger value"), "got: {msg}");
}
#[test]
fn linger_unexpected_format() {
let result = parse_linger_output("something else");
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("Unexpected loginctl output format"),
"got: {msg}"
);
}
#[test]
fn sandbox_entitlement_present_and_true() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
</dict>
</plist>"#;
assert!(parse_entitlements_for_sandbox(xml));
}
#[test]
fn sandbox_entitlement_present_but_false() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<false/>
</dict>
</plist>"#;
assert!(!parse_entitlements_for_sandbox(xml));
}
#[test]
fn no_sandbox_entitlement() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>"#;
assert!(!parse_entitlements_for_sandbox(xml));
}
#[test]
fn empty_entitlements() {
assert!(!parse_entitlements_for_sandbox(""));
}
#[test]
fn sandbox_with_other_entitlements() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
</dict>
</plist>"#;
assert!(parse_entitlements_for_sandbox(xml));
}
}