use crate::constants::{JAVA_HOME_BIN, JVR_BEGIN, JVR_END, PATH_ENVIRONMENT_VARIABLE_UNIX};
use crate::env::EnvironmentAccessor;
use std::fs::{File, OpenOptions};
use std::io::{self, BufRead, BufReader, Read, Write};
use std::path::{Path, PathBuf};
pub struct UnixEnvironmentAccessor {}
impl UnixEnvironmentAccessor {
pub fn new() -> Self {
Self {}
}
fn get_home_dir(&self) -> io::Result<PathBuf> {
dirs::home_dir().ok_or_else(|| {
io::Error::new(io::ErrorKind::NotFound, "Cannot determine home directory")
})
}
fn detect_shell(&self) -> String {
std::env::var("SHELL")
.unwrap_or_else(|_| "/bin/bash".to_string())
.to_lowercase()
}
fn get_user_shell_config_files(&self) -> Vec<PathBuf> {
let home = match self.get_home_dir() {
Ok(h) => h,
Err(_) => return vec![],
};
let shell = self.detect_shell();
let mut config_files = Vec::new();
if shell.contains("zsh") {
config_files.push(home.join(".zshrc"));
config_files.push(home.join(".zshenv"));
} else if shell.contains("bash") {
config_files.push(home.join(".bashrc"));
config_files.push(home.join(".bash_profile"));
config_files.push(home.join(".profile"));
} else {
config_files.push(home.join(".profile"));
config_files.push(home.join(".bashrc"));
}
config_files
}
fn get_system_config_files(&self) -> Vec<PathBuf> {
vec![
PathBuf::from("/etc/profile"),
PathBuf::from("/etc/environment"),
PathBuf::from("/etc/bash.bashrc"),
]
}
fn read_env_from_file(&self, file_path: &Path, name: &str) -> io::Result<Option<String>> {
if !file_path.exists() {
return Ok(None);
}
let file = File::open(file_path)?;
let reader = BufReader::new(file);
for line in reader.lines() {
let line = line?;
let line = line.trim();
if line.starts_with("export") {
if let Some(rest) = line.strip_prefix("export") {
if let Some(value) = self.parse_env_line(rest.trim(), name) {
return Ok(Some(value));
}
}
} else if let Some(value) = self.parse_env_line(line, name) {
return Ok(Some(value));
}
}
Ok(None)
}
fn parse_env_line(&self, line: &str, name: &str) -> Option<String> {
if line.starts_with('#') {
return None;
}
if let Some(equals_pos) = line.find('=') {
let var_name = line[..equals_pos].trim();
if var_name == name {
let value = line[equals_pos + 1..].trim();
let value = value
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.or_else(|| value.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
.unwrap_or(value);
return Some(value.to_string());
}
}
None
}
fn write_env_to_file(&self, file_path: &Path, name: &str, value: &str) -> io::Result<()> {
let mut content = String::new();
if file_path.exists() {
File::open(file_path)?.read_to_string(&mut content)?;
}
let quoted = self.quote_value(value);
let export_assign = format!("export {}={}", name, quoted);
let assign = format!("{}={}", name, quoted);
let mut has_export = false;
let mut has_export_assign = false;
let mut has_assign = false;
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with('#') {
continue;
}
let (exported, export_assigned) = self.export_line_contains(trimmed, name);
if exported {
has_export = true;
}
if export_assigned {
has_export_assign = true;
}
if self.is_assign_line(trimmed, name) {
has_assign = true;
}
}
let mut new_lines = Vec::new();
for line in content.lines() {
let trimmed = line.trim();
if has_export_assign {
let (_, export_assigned) = self.export_line_contains(trimmed, name);
if export_assigned {
new_lines.push(export_assign.clone());
continue;
}
}
if !has_export_assign && has_assign && has_export && self.is_assign_line(trimmed, name)
{
new_lines.push(assign.clone());
continue;
}
new_lines.push(line.to_string());
}
if !has_export_assign && !(has_assign && has_export) {
if !new_lines.is_empty() {
new_lines.push(String::new());
}
new_lines.push(self.build_managed_block(&export_assign));
}
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(file_path)?;
for line in new_lines {
writeln!(file, "{}", line)?;
}
Ok(())
}
fn quote_value(&self, value: &str) -> String {
if value.contains(' ') || value.contains('$') || value.contains('`') {
format!("\"{}\"", value.replace('"', "\\\""))
} else {
value.to_string()
}
}
fn export_line_contains(&self, line: &str, name: &str) -> (bool, bool) {
let trimmed = line.trim();
if !trimmed.starts_with("export ") {
return (false, false);
}
let rest = trimmed.strip_prefix("export ").unwrap();
let mut has_export = false;
let mut has_export_assign = false;
for token in rest.split_whitespace() {
if token == name {
has_export = true;
} else if token.starts_with(&format!("{}=", name)) {
has_export = true;
has_export_assign = true;
}
}
(has_export, has_export_assign)
}
fn is_assign_line(&self, line: &str, name: &str) -> bool {
let trimmed = line.trim();
trimmed.starts_with(&format!("{}=", name))
}
fn build_managed_block(&self, export_assign: &str) -> String {
format!(
"{}\n\
{}\n\
export PATH=$PATH:{}\n\
{}",
JVR_BEGIN, export_assign, JAVA_HOME_BIN, JVR_END
)
}
fn find_env_in_files(&self, files: &[PathBuf], name: &str) -> io::Result<String> {
for file in files {
if let Ok(Some(value)) = self.read_env_from_file(file, name) {
return Ok(value);
}
}
Err(io::Error::new(
io::ErrorKind::NotFound,
format!("Environment variable '{}' not found", name),
))
}
fn write_env_to_first_available(
&self,
files: &[PathBuf],
name: &str,
value: &str,
) -> io::Result<()> {
for file in files {
if file.exists() || file.parent().map(|p| p.exists()).unwrap_or(false) {
return self.write_env_to_file(file, name, value);
}
}
if let Some(first) = files.first() {
if let Some(parent) = first.parent() {
std::fs::create_dir_all(parent)?;
}
self.write_env_to_file(first, name, value)
} else {
Err(io::Error::new(
io::ErrorKind::InvalidInput,
"No configuration file available",
))
}
}
fn ensure_path_contains(&self, entry: &str, user: bool) -> io::Result<()> {
let files = if user {
self.get_user_shell_config_files()
} else {
self.get_system_config_files()
};
let current_path = self
.find_env_in_files(&files, PATH_ENVIRONMENT_VARIABLE_UNIX)
.unwrap_or_else(|_| std::env::var(PATH_ENVIRONMENT_VARIABLE_UNIX).unwrap_or_default());
let paths: Vec<&str> = current_path.split(':').collect();
let found = paths.iter().any(|p| p.trim() == entry.trim());
if !found {
let new_path = if current_path.is_empty() {
entry.to_string()
} else {
format!("{}:{}", current_path, entry)
};
self.write_env_to_first_available(&files, PATH_ENVIRONMENT_VARIABLE_UNIX, &new_path)?;
}
Ok(())
}
}
impl Default for UnixEnvironmentAccessor {
fn default() -> Self {
Self::new()
}
}
impl EnvironmentAccessor for UnixEnvironmentAccessor {
fn set_user_environment_variable(&self, name: &str, value: &str) -> io::Result<()> {
let files = self.get_user_shell_config_files();
self.write_env_to_first_available(&files, name, value)
}
fn get_user_environment_variable(&self, name: &str) -> io::Result<String> {
let files = self.get_user_shell_config_files();
self.find_env_in_files(&files, name)
}
fn set_system_environment_variable(&self, name: &str, value: &str) -> io::Result<()> {
let files = self.get_system_config_files();
self.write_env_to_first_available(&files, name, value)
}
fn get_system_environment_variable(&self, name: &str) -> io::Result<String> {
let files = self.get_system_config_files();
self.find_env_in_files(&files, name)
}
fn broadcast_environment_change(&self) -> io::Result<()> {
Ok(())
}
fn ensure_java_home_bin_in_user_path(&self) -> io::Result<()> {
self.ensure_path_contains(JAVA_HOME_BIN, true)
}
fn ensure_java_home_bin_in_system_path(&self) -> io::Result<()> {
self.ensure_path_contains(JAVA_HOME_BIN, false)
}
}