use std::fs;
use arcbox_helper::{HOSTS_ALIAS_LINE as MANAGED_LINE, hosts_alias_installed};
const HOSTS_PATH: &str = "/etc/hosts";
const HOSTS_TMP_PATH: &str = "/etc/hosts.arcbox.tmp";
const MARKER: &str = "# managed by arcbox-helper";
pub fn install() -> Result<(), String> {
let content = read_hosts()?;
match with_alias(&content) {
Some(updated) => write_hosts(&updated),
None => Ok(()),
}
}
pub fn uninstall() -> Result<(), String> {
let content = read_hosts()?;
match without_alias(&content) {
Some(updated) => write_hosts(&updated),
None => Ok(()),
}
}
pub fn status() -> Result<bool, String> {
Ok(hosts_alias_installed(&read_hosts()?))
}
fn with_alias(content: &str) -> Option<String> {
if hosts_alias_installed(content) {
return None;
}
let mut updated = content.to_string();
if !updated.is_empty() && !updated.ends_with('\n') {
updated.push('\n');
}
updated.push_str(MANAGED_LINE);
updated.push('\n');
Some(updated)
}
fn without_alias(content: &str) -> Option<String> {
if !content.lines().any(is_managed) {
return None;
}
let mut updated: String = content
.lines()
.filter(|line| !is_managed(line))
.collect::<Vec<_>>()
.join("\n");
if !updated.is_empty() {
updated.push('\n');
}
Some(updated)
}
fn is_managed(line: &str) -> bool {
line.contains(MARKER)
}
fn read_hosts() -> Result<String, String> {
fs::read_to_string(HOSTS_PATH).map_err(|e| format!("failed to read {HOSTS_PATH}: {e}"))
}
fn write_hosts(content: &str) -> Result<(), String> {
use std::os::unix::fs::PermissionsExt;
fs::write(HOSTS_TMP_PATH, content)
.map_err(|e| format!("failed to write {HOSTS_TMP_PATH}: {e}"))?;
fs::set_permissions(HOSTS_TMP_PATH, fs::Permissions::from_mode(0o644))
.map_err(|e| format!("failed to chmod {HOSTS_TMP_PATH}: {e}"))?;
fs::rename(HOSTS_TMP_PATH, HOSTS_PATH).map_err(|e| {
let _ = fs::remove_file(HOSTS_TMP_PATH);
format!("failed to replace {HOSTS_PATH}: {e}")
})
}
#[cfg(test)]
mod tests {
use super::{MANAGED_LINE, MARKER, with_alias, without_alias};
const BASE: &str = "##\n127.0.0.1\tlocalhost\n255.255.255.255\tbroadcasthost\n";
#[test]
fn managed_line_carries_the_uninstall_marker() {
assert!(MANAGED_LINE.ends_with(MARKER));
}
#[test]
fn install_appends_one_managed_line() {
let updated = with_alias(BASE).expect("alias missing, should rewrite");
assert!(updated.starts_with(BASE));
assert!(updated.ends_with(&format!("{MANAGED_LINE}\n")));
assert_eq!(with_alias(&updated), None);
}
#[test]
fn install_handles_missing_trailing_newline() {
let updated = with_alias("127.0.0.1\tlocalhost").expect("should rewrite");
assert_eq!(updated, format!("127.0.0.1\tlocalhost\n{MANAGED_LINE}\n"));
}
#[test]
fn uninstall_removes_only_managed_lines() {
let installed = with_alias(BASE).expect("should rewrite");
let removed = without_alias(&installed).expect("managed line present");
assert_eq!(removed, BASE);
assert_eq!(without_alias(&removed), None);
}
#[test]
fn uninstall_keeps_user_arcbox_entries() {
let content = format!("10.0.0.1\tArcBox\n{MANAGED_LINE}\n");
let removed = without_alias(&content).expect("managed line present");
assert_eq!(removed, "10.0.0.1\tArcBox\n");
}
}