use crate::error::{Error, Result};
use crate::types::FirefoxInstallation;
use std::fs;
use std::path::{Path, PathBuf};
#[cfg(target_os = "linux")]
const FIREFOX_SEARCH_PATHS: &[&str] = &[
"/usr/lib/firefox",
"/usr/lib64/firefox",
"/opt/firefox",
"/usr/local/firefox",
"/opt/firefox-beta",
"/opt/firefox-esr",
];
#[cfg(target_os = "macos")]
const FIREFOX_SEARCH_PATHS: &[&str] = &[
"/Applications/Firefox.app/Contents/Resources",
"/Applications/Firefox Beta.app/Contents/Resources",
"/Applications/Firefox Developer Edition.app/Contents/Resources",
"/Applications/Firefox ESR.app/Contents/Resources",
];
#[cfg(target_os = "windows")]
const FIREFOX_SEARCH_PATHS: &[&str] = &[
r"C:\Program Files\Mozilla Firefox",
r"C:\Program Files\Firefox Beta",
r"C:\Program Files\Firefox ESR",
r"C:\Program Files\Mozilla Firefox ESR",
r"C:\Program Files (x86)\Mozilla Firefox",
r"C:\Program Files (x86)\Firefox Beta",
r"C:\Program Files (x86)\Firefox ESR",
r"C:\Program Files (x86)\Mozilla Firefox ESR",
r"C:\Program Files\Mozilla Firefox Developer Edition",
];
#[cfg(target_os = "linux")]
const NIX_STORE_PATHS: &[&str] = &[
"/nix/var/nix/profiles/default/bin/firefox",
"/run/current-system/sw/bin/firefox",
"/etc/profiles",
];
pub fn find_firefox_installation() -> Result<Option<FirefoxInstallation>> {
let search_paths = get_all_search_paths();
for path in search_paths {
if let Ok(install) = validate_installation(&path) {
return Ok(Some(install));
}
}
Ok(None)
}
pub fn find_all_firefox_installations() -> Result<Vec<FirefoxInstallation>> {
let mut installations = Vec::new();
let search_paths = get_all_search_paths();
for path in search_paths {
if let Ok(install) = validate_installation(&path) {
installations.push(install);
}
}
Ok(installations)
}
pub fn get_firefox_version(install_path: &Path) -> Result<String> {
let app_ini = install_path.join("application.ini");
if app_ini.exists() {
return extract_version_from_ini(&app_ini);
}
let platform_ini = install_path.join("platform.ini");
if platform_ini.exists() {
return extract_version_from_ini(&platform_ini);
}
#[cfg(target_os = "macos")]
{
let resources_ini = install_path.join("application.ini");
if resources_ini.exists() {
return extract_version_from_ini(&resources_ini);
}
}
#[cfg(target_os = "macos")]
{
if let Some(parent) = install_path.parent() {
let browser_ini = parent.join("browserconfig.properties");
if browser_ini.exists() {
if let Ok(content) = fs::read_to_string(&browser_ini) {
for line in content.lines() {
if line.contains("version") {
if let Some(version) = line.split('=').nth(1) {
return Ok(version.trim().to_string());
}
}
}
}
}
}
}
Err(Error::FirefoxNotFound {
searched_paths: format!("{} (no version info found)", install_path.display()),
})
}
fn validate_installation(path: &str) -> Result<FirefoxInstallation> {
let install_path = PathBuf::from(path);
if !install_path.exists() {
return Err(Error::FirefoxNotFound {
searched_paths: path.to_string(),
});
}
let omni_ja_paths = [
install_path.join("browser/omni.ja"),
install_path.join("omni.ja"),
];
let has_omni_ja = omni_ja_paths.iter().any(|p| p.exists());
let greprefs_paths = [
install_path.join("greprefs.js"),
install_path.join("browser/greprefs.js"),
];
let has_greprefs = greprefs_paths.iter().any(|p| p.exists());
if !has_omni_ja && !has_greprefs {
return Err(Error::FirefoxNotFound {
searched_paths: format!("{} (no omni.ja or greprefs.js found)", path),
});
}
let version = get_firefox_version(&install_path).unwrap_or_else(|_| "unknown".to_string());
Ok(FirefoxInstallation {
version,
path: install_path,
has_greprefs,
has_omni_ja,
})
}
fn extract_version_from_ini(ini_path: &Path) -> Result<String> {
let content = fs::read_to_string(ini_path).map_err(|e| Error::FirefoxNotFound {
searched_paths: format!("{} (cannot read: {})", ini_path.display(), e),
})?;
for line in content.lines() {
let line = line.trim();
if line.starts_with("Version=") || line.starts_with("Version =") {
if let Some(version) = line.split('=').nth(1) {
return Ok(version.trim().to_string());
}
}
}
Err(Error::FirefoxNotFound {
searched_paths: format!("{} (no version found)", ini_path.display()),
})
}
fn get_all_search_paths() -> Vec<String> {
let mut paths: Vec<String> = FIREFOX_SEARCH_PATHS.iter().map(|s| s.to_string()).collect();
#[cfg(target_os = "linux")]
{
for nix_path in NIX_STORE_PATHS {
let nix_path_buf = PathBuf::from(nix_path);
if nix_path_buf.exists() || nix_path_buf.is_symlink() {
if let Ok(canonical) = nix_path_buf.canonicalize() {
if canonical.ends_with("firefox") || canonical.ends_with("firefox-bin") {
if let Some(bin_dir) = canonical.parent() {
if let Some(store_base) = bin_dir.parent() {
let lib_firefox = store_base.join("lib/firefox");
if lib_firefox.exists() {
paths.push(lib_firefox.to_string_lossy().to_string());
}
}
}
}
}
}
if nix_path_buf.is_dir() {
let entries = walk_dir_depth(&nix_path_buf, 4);
for entry in entries {
if entry.ends_with("firefox") || entry.ends_with("firefox-bin") {
if let Ok(canonical) = entry.canonicalize() {
if let Some(bin_dir) = canonical.parent() {
if let Some(store_base) = bin_dir.parent() {
let lib_firefox = store_base.join("lib/firefox");
if lib_firefox.exists() {
paths.push(lib_firefox.to_string_lossy().to_string());
}
}
}
}
}
}
}
}
}
let mut seen = std::collections::HashSet::new();
paths.retain(|p| seen.insert(p.clone()));
paths
}
#[cfg(target_os = "linux")]
fn walk_dir_depth(dir: &Path, max_depth: usize) -> Vec<PathBuf> {
let mut results = Vec::new();
let mut current_dirs = vec![dir.to_path_buf()];
for _depth in 0..max_depth {
let mut next_dirs = Vec::new();
for dir_path in ¤t_dirs {
if let Ok(entries) = fs::read_dir(dir_path) {
for entry in entries.flatten() {
let path = entry.path();
let file_name = path.file_name();
let is_firefox = file_name.is_some() && {
let name = file_name.unwrap().to_string_lossy();
name == "firefox" || name == "firefox-bin"
};
if is_firefox {
results.push(path.clone());
}
if path.is_dir() {
next_dirs.push(path);
}
}
}
}
current_dirs = next_dirs;
if current_dirs.is_empty() {
break;
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_all_search_paths_not_empty() {
let paths = get_all_search_paths();
assert!(!paths.is_empty());
}
#[test]
fn test_validate_nonexistent_path() {
let result = validate_installation("/nonexistent/firefox/path/xyz123");
assert!(result.is_err());
}
#[cfg(target_os = "linux")]
#[test]
fn test_firefox_search_paths_include_standard_locations() {
let paths = get_all_search_paths();
assert!(paths.iter().any(|p| p.contains("firefox")));
}
#[test]
fn test_extract_version_from_ini_content() {
let ini_content = r#"
[App]
Version=128.0
Name=Firefox
"#;
assert!(ini_content.contains("Version=128.0"));
}
}