pub(super) fn validate_value(value: &str) -> Result<(), String> {
if value.is_empty() {
return Err("an empty value".to_string());
}
match value
.chars()
.find(|c| matches!(c, '#' | '$' | '"' | '\'' | '\\' | '\n' | '\r'))
{
Some(c) => Err(format!(
"the value holds {c:?}, which environment.d cannot quote"
)),
None => Ok(()),
}
}
#[cfg_attr(any(target_os = "macos", windows), allow(dead_code))]
pub(super) fn clear_commands(name: &str) -> Vec<(&'static str, Vec<String>)> {
vec![
(
"dbus-update-activation-environment",
vec![format!("{name}=")],
),
(
"systemctl",
vec![
"--user".to_string(),
"unset-environment".to_string(),
name.to_string(),
],
),
]
}
#[cfg_attr(any(target_os = "macos", windows), allow(dead_code))]
pub(super) fn value_of(lines: Vec<(String, String)>, name: &str) -> Option<String> {
lines
.into_iter()
.find(|(k, _)| k == name)
.map(|(_, v)| v)
.filter(|v| !v.is_empty())
}
#[cfg(not(any(target_os = "macos", windows)))]
pub(super) mod session {
use std::path::PathBuf;
use super::super::run_checked;
use super::{clear_commands, validate_value, value_of};
const CONF_FILE: &str = "60-openlatch-cline.conf";
pub(in super::super) const MANUAL_CLEAR: &str =
"`systemctl --user unset-environment CLINE_WRAPPER_PATH CLINE_JS_RUNTIME_PATH` and by deleting \
`~/.config/environment.d/60-openlatch-cline.conf`";
fn conf_path() -> Option<PathBuf> {
dirs::config_dir().map(|d| d.join("environment.d").join(CONF_FILE))
}
fn read_conf() -> Vec<(String, String)> {
let Some(path) = conf_path() else {
return Vec::new();
};
std::fs::read_to_string(path)
.map(|raw| parse_lines(&raw))
.unwrap_or_default()
}
fn conf_without(name: &str) -> Vec<(String, String)> {
read_conf().into_iter().filter(|(k, _)| k != name).collect()
}
fn parse_lines(raw: &str) -> Vec<(String, String)> {
raw.lines()
.map(str::trim)
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.filter_map(|l| l.split_once('='))
.map(|(k, v)| (k.trim().to_string(), v.to_string()))
.collect()
}
fn write_conf(lines: &[(String, String)]) -> Result<(), String> {
let path = conf_path().ok_or("no user configuration directory")?;
if lines.is_empty() {
return match std::fs::remove_file(&path) {
Err(e) if e.kind() != std::io::ErrorKind::NotFound => Err(e.to_string()),
_ => Ok(()),
};
}
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
}
let mut body = String::from(
"# Written by OpenLatch for the Cline plugin. Removed by `openlatch uninstall`.\n",
);
for (k, v) in lines {
body.push_str(&format!("{k}={v}\n"));
}
crate::fs_secure::write_readable(&path, &body).map_err(|e| e.to_string())
}
fn best_effort(program: &str, args: &[&str], what: &str) {
if let Err(e) = run_checked(program, args) {
tracing::debug!(error = %e, "Cline login environment: {what}");
}
}
pub(in super::super) fn read(name: &str) -> Result<Option<String>, String> {
if let Ok(out) = run_checked("systemctl", &["--user", "show-environment"]) {
if let Some(v) = value_of(parse_lines(&out), name) {
return Ok(Some(v));
}
}
Ok(value_of(read_conf(), name))
}
pub(in super::super) fn apply(name: &str, value: &str) -> Result<(), String> {
validate_value(value)?;
let mut lines = conf_without(name);
lines.push((name.to_string(), value.to_string()));
write_conf(&lines)?;
let pair = format!("{name}={value}");
best_effort(
"systemctl",
&["--user", "set-environment", &pair],
"session copy not updated",
);
best_effort(
"dbus-update-activation-environment",
&["--systemd", &pair],
"session copy not updated",
);
Ok(())
}
pub(in super::super) fn clear(name: &str) -> Result<(), String> {
write_conf(&conf_without(name))?;
for (program, args) in clear_commands(name) {
let args: Vec<&str> = args.iter().map(String::as_str).collect();
best_effort(program, &args, "session copy not cleared");
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::{clear_commands, validate_value, value_of};
#[test]
fn linux_clear_blanks_dbus_then_unsets_the_user_manager() {
let commands = clear_commands("CLINE_WRAPPER_PATH");
assert_eq!(
commands,
vec![
(
"dbus-update-activation-environment",
vec!["CLINE_WRAPPER_PATH=".to_string()]
),
(
"systemctl",
vec![
"--user".to_string(),
"unset-environment".to_string(),
"CLINE_WRAPPER_PATH".to_string()
]
),
]
);
assert!(
commands
.iter()
.all(|(_, args)| !args.iter().any(|a| a == "--systemd")),
"clear must not write an empty value into the user manager"
);
}
#[test]
fn linux_empty_value_reads_as_none() {
let lines = |v: &str| {
vec![
("A".to_string(), "x".to_string()),
("N".to_string(), v.to_string()),
]
};
assert_eq!(value_of(lines(""), "N"), None);
assert_eq!(value_of(lines("/p"), "N"), Some("/p".to_string()));
assert_eq!(value_of(lines("/p"), "M"), None);
}
#[test]
fn linux_refuses_unquotable_values() {
for bad in [
"/a#b", "/a$b", "/a\"b", "/a'b", "/a\\b", "/a\nb", "/a\rb", "",
] {
assert!(validate_value(bad).is_err(), "{bad:?} was accepted");
}
for good in [
"/Users/dev/.openlatch/bin/cline-js-runtime-vscode",
"/home/dev/my openlatch/cline-plugin-bootstrap/wrapper",
] {
assert_eq!(validate_value(good), Ok(()), "{good:?}");
}
}
}