util.rs
1 use std::{ 2 env::{self, current_exe}, 3 fs::{self, File}, 4 io::{BufReader, Read as _}, 5 path::{Path, PathBuf}, 6 process::{Command, exit}, 7 }; 8 9 use anyhow::{Context as _, Result, anyhow}; 10 11 use crate::{SILENT, config::CONFIG}; 12 13 /// The absolute path to the users home directory. 14 pub fn home() -> Result<String> { 15 env::var("HOME").context("Failed to get HOME env variable") 16 } 17 18 pub fn get_hostname() -> Result<String> { 19 Ok(fs::read_to_string("/etc/hostname") 20 .context("Failed to read /etc/hostname")? 21 .trim() 22 .into()) 23 } 24 25 /// Inform the user of the `failed_action` and rerun with root privileges 26 #[expect(clippy::expect_used)] // We dont return anyways, so we might as well panic 27 pub fn rerun_with_root(failed_action: &str) -> ! { 28 if !SILENT.get().expect("Failed to get SILENT") { 29 println!("{failed_action} requires root privileges",); 30 } 31 rerun_with_root_args(&[]); 32 } 33 34 /// Rerun with root privileges, and add the provided args to the command 35 #[expect(clippy::expect_used)] // We dont return anyways, so we might as well panic 36 pub fn rerun_with_root_args(args: &[&str]) -> ! { 37 // Collect args 38 let mut args: Vec<_> = env::args() 39 .chain(args.iter().map(|&str| str.to_owned())) 40 .collect(); 41 42 // Overwrite the exe path with the absolute path if possible 43 if let Some(absolute_path) = current_exe() 44 .ok() 45 .and_then(|path| path.to_str().map(ToOwned::to_owned)) 46 { 47 args[0] = absolute_path; 48 } 49 50 let home = home().expect("Failed to get users home di"); 51 52 let status = Command::new("/usr/bin/sudo") 53 // Preserve $HOME 54 .arg(format!("HOME={home}")) 55 .args(args) 56 .spawn() 57 .expect("Failed to spawn child process") 58 .wait() 59 .expect("Failed to wait on child process"); 60 61 if status.success() { 62 exit(0); 63 } else { 64 exit(status.code().unwrap_or(1)); 65 } 66 } 67 68 /// Converts the path relative to files/ to the location on the actual system. (by trimming the subdir of files/ away) 69 pub fn system_path(path: &Path) -> Result<PathBuf> { 70 let str = path 71 .as_os_str() 72 .to_str() 73 .context("Failed to convert path to string")?; 74 75 // Replace {home} with the users home dir 76 let resolved_home = str.replace("{home}", &home()?[1..]); 77 78 Ok(if path.is_relative() { 79 let index = str 80 .find('/') 81 .with_context(|| format!("Failed finding '/' in path '{}'", path.display()))?; 82 83 // Only keep the path from the first / 84 let absolute = &resolved_home[index..]; 85 86 absolute.into() 87 } else { 88 // The default subdir was elided, so the path is already the correct one 89 resolved_home.into() 90 }) 91 } 92 93 /// Converts the path that should be symlinked to the path in the files/ directory 94 #[expect(clippy::literal_string_with_formatting_args)] 95 pub fn config_path(mut cli_path: &Path) -> Result<PathBuf> { 96 assert!( 97 !Path::new(&CONFIG.default_subdir).is_absolute(), 98 "Default subdir is not allowed to be absolute" 99 ); 100 101 let mut config_path = PathBuf::from(&CONFIG.files_path); 102 103 // If the path started with "/", the default subdir was elided 104 if let Ok(relative_path) = cli_path.strip_prefix("/") { 105 // So we add it 106 config_path.push(&CONFIG.default_subdir); 107 108 // And replace the absolute path with the relative one to avoid overwriting the entire config_path 109 cli_path = relative_path; 110 } 111 // If the default subdir wasn't elided, replace "{hostname}" with the actual hostname 112 else if let Ok(stripped_path) = cli_path.strip_prefix("{hostname}") { 113 let hostname = get_hostname(); 114 config_path.push(hostname?.trim()); 115 116 cli_path = stripped_path; 117 } 118 119 // Replace "{home}" with the users home dir 120 if let Ok(stripped_path) = cli_path.strip_prefix("{home}") { 121 let home = home()?; 122 config_path.push(&home[1..]); // skip the leading '/' to avoid overwriting the entire config_path 123 124 cli_path = stripped_path; 125 } 126 127 config_path.push(cli_path); 128 129 Ok(config_path) 130 } 131 132 /// Checks if the config & system paths are already equal 133 /// Does *not* currently support directories 134 #[expect(clippy::filetype_is_file)] 135 pub fn paths_equal(config_path: &Path, system_path: &Path) -> Result<()> { 136 let fmt_diff = |difference: &str| { 137 Err(anyhow!( 138 "Path {} already exists and differs in {difference} to {}", 139 system_path.display(), 140 config_path.display() 141 )) 142 }; 143 144 // Get metadatas 145 let system_metadata = fs::symlink_metadata(system_path).with_context(|| { 146 format!( 147 "Failed to get metadata for system path {}", 148 system_path.display() 149 ) 150 })?; 151 let config_metadata = fs::symlink_metadata(config_path).with_context(|| { 152 format!( 153 "Failed to get metadata for config path {}", 154 config_path.display() 155 ) 156 })?; 157 158 if system_metadata.file_type() != config_metadata.file_type() { 159 fmt_diff("file type") 160 } else if system_metadata.len() != config_metadata.len() { 161 fmt_diff("length") 162 } else if system_metadata.permissions() != config_metadata.permissions() { 163 fmt_diff("permissions") 164 // If they are symlinks 165 } else if system_metadata.file_type().is_symlink() 166 // And their destinations dont match 167 && fs::read_link(system_path).with_context(|| format!("reading symlink destination for path {}", system_path.display()))? 168 != fs::read_link(config_path).with_context(|| format!("reading symlink destination for path {}", config_path.display()))? 169 { 170 fmt_diff("symlink destination") 171 } else if system_metadata.file_type().is_file() { 172 let system_file = File::open(system_path).context("opening system file")?; 173 let config_file = File::open(config_path).context("opening config file")?; 174 175 let mut system_reader = BufReader::new(system_file); 176 let mut config_reader = BufReader::new(config_file); 177 178 let mut system_buf = [0; 4096]; 179 let mut config_buf = [0; 4096]; 180 181 loop { 182 let system_read = system_reader 183 .read(&mut system_buf) 184 .context("reading system file")?; 185 186 let config_read = config_reader 187 .read(&mut config_buf) 188 .context("reading config file")?; 189 190 if system_read != config_read { 191 fmt_diff("content length")?; 192 } else if system_read == 0 { 193 return Ok(()); // EOF & identical 194 } else if system_buf[..system_read] != config_buf[..config_read] { 195 fmt_diff("file contents")?; 196 } 197 } 198 } else { 199 Ok(()) 200 } 201 }