use crate::error::{Error, Result};
use crate::firefox_locator;
use crate::omni_extractor::{ExtractConfig, OmniExtractor};
use crate::parser::parse_prefs_js_file;
use crate::types::{MergedPreferences, PrefEntry, PrefSource};
use std::collections::HashMap;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct MergeConfig {
pub include_builtins: bool,
pub include_globals: bool,
pub include_user: bool,
pub continue_on_error: bool,
}
impl Default for MergeConfig {
fn default() -> Self {
Self {
include_builtins: true,
include_globals: true,
include_user: true,
continue_on_error: true,
}
}
}
pub fn merge_all_preferences(
profile_path: &Path,
install_path: Option<&Path>,
config: &MergeConfig,
) -> Result<MergedPreferences> {
let mut warnings = Vec::new();
let mut loaded_sources = Vec::new();
let mut pref_map: HashMap<String, PrefEntry> = HashMap::new();
let resolved_install_path = if let Some(path) = install_path {
Some(path.to_path_buf())
} else if config.include_builtins || config.include_globals {
match firefox_locator::find_firefox_installation() {
Ok(Some(install)) => {
loaded_sources.push(PrefSource::BuiltIn);
Some(install.path)
}
Ok(None) => {
warnings.push("Firefox installation not found".to_string());
None
}
Err(e) => {
warnings.push(format!("Failed to locate Firefox: {}", e));
None
}
}
} else {
None
};
if config.include_builtins {
if let Some(ref install) = resolved_install_path {
match load_builtin_preferences(install, &mut warnings) {
Ok(builtins) => {
for pref in builtins {
pref_map.insert(pref.key.clone(), pref);
}
loaded_sources.push(PrefSource::BuiltIn);
}
Err(e) => {
let msg = format!("Failed to load built-in preferences: {}", e);
warnings.push(msg.clone());
if !config.continue_on_error {
return Err(Error::OmniJaError(msg));
}
}
}
}
}
if config.include_globals {
if let Some(ref install) = resolved_install_path {
match load_global_preferences(install, &mut warnings) {
Ok(globals) => {
for pref in globals {
pref_map.insert(pref.key.clone(), pref);
}
loaded_sources.push(PrefSource::GlobalDefault);
}
Err(e) => {
let msg = format!("Failed to load global preferences: {}", e);
warnings.push(msg);
if !config.continue_on_error {
return Err(Error::PrefFileNotFound {
file: "greprefs.js".to_string(),
});
}
}
}
}
}
if config.include_user {
let prefs_js_path = profile_path.join("prefs.js");
match load_user_preferences(&prefs_js_path, &mut warnings) {
Ok(user_prefs) => {
for pref in user_prefs {
pref_map.insert(pref.key.clone(), pref);
}
loaded_sources.push(PrefSource::User);
}
Err(e) => {
let msg = format!("Failed to load user preferences: {}", e);
warnings.push(msg);
if !config.continue_on_error {
return Err(e);
}
}
}
}
let mut entries: Vec<PrefEntry> = pref_map.into_values().collect();
entries.sort_by(|a, b| a.key.cmp(&b.key));
Ok(MergedPreferences {
entries,
install_path: resolved_install_path,
profile_path: profile_path.to_path_buf(),
loaded_sources,
warnings,
})
}
pub fn get_effective_pref<'a>(prefs: &'a [PrefEntry], key: &str) -> Option<&'a PrefEntry> {
prefs.iter().find(|e| e.key == key)
}
fn load_builtin_preferences(
install_path: &Path,
warnings: &mut Vec<String>,
) -> Result<Vec<PrefEntry>> {
let omni_paths = [
install_path.join("browser/omni.ja"),
install_path.join("omni.ja"),
];
let omni_path = omni_paths.iter().find(|p| p.exists()).ok_or_else(|| {
warnings.push("omni.ja not found in Firefox installation".to_string());
Error::PrefFileNotFound {
file: "omni.ja".to_string(),
}
})?;
let config = ExtractConfig {
target_files: vec![
"defaults/preferences/*.js".to_string(),
"defaults/pref/*.js".to_string(),
],
..Default::default()
};
let extractor = OmniExtractor::with_config(omni_path.clone(), config)?;
let extracted_files = extractor.extract_prefs()?;
let mut all_prefs = Vec::new();
for file_path in extracted_files {
match parse_prefs_js_file(&file_path) {
Ok(mut prefs) => {
for pref in &mut prefs {
pref.source = Some(PrefSource::BuiltIn);
if let Ok(file_name) = file_path.strip_prefix(install_path) {
pref.source_file = Some(format!("omni.ja:{}", file_name.display()));
}
}
all_prefs.extend(prefs);
}
Err(e) => {
let file_name = file_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown");
if file_name != "firefox.js" {
warnings.push(format!("Failed to parse {}: {}", file_path.display(), e));
}
}
}
}
Ok(all_prefs)
}
fn load_global_preferences(
install_path: &Path,
warnings: &mut Vec<String>,
) -> Result<Vec<PrefEntry>> {
let greprefs_paths = [
install_path.join("greprefs.js"),
install_path.join("browser/greprefs.js"),
];
let greprefs_path = if let Some(path) = greprefs_paths.iter().find(|p| p.exists()) {
path.clone()
} else {
let omni_paths = [
install_path.join("omni.ja"),
install_path.join("browser/omni.ja"),
];
let omni_path = omni_paths.iter().find(|p| p.exists()).ok_or_else(|| {
warnings.push("greprefs.js not found and omni.ja not found".to_string());
Error::PrefFileNotFound {
file: "greprefs.js".to_string(),
}
})?;
let config = ExtractConfig {
target_files: vec!["greprefs.js".to_string()],
..Default::default()
};
let extractor = OmniExtractor::with_config(omni_path.clone(), config)?;
let extracted_files = match extractor.extract_prefs() {
Ok(files) => files,
Err(e) => {
warnings.push(format!("Failed to extract greprefs.js from omni.ja: {}", e));
return Err(Error::PrefFileNotFound {
file: "greprefs.js".to_string(),
});
}
};
if extracted_files.is_empty() {
warnings.push("greprefs.js not found in omni.ja".to_string());
return Err(Error::PrefFileNotFound {
file: "greprefs.js".to_string(),
});
}
let temp_dir = tempfile::tempdir()?;
let temp_path = temp_dir.path().join("greprefs.js");
std::fs::copy(&extracted_files[0], &temp_path)?;
let _ = Box::leak(Box::new(temp_dir));
temp_path
};
let mut prefs = parse_prefs_js_file(&greprefs_path).unwrap_or_default();
let source_file = if greprefs_path.to_string_lossy().contains("omni") {
"omni.ja:greprefs.js".to_string()
} else {
"greprefs.js".to_string()
};
for pref in &mut prefs {
pref.source = Some(PrefSource::GlobalDefault);
pref.source_file = Some(source_file.clone());
}
Ok(prefs)
}
fn load_user_preferences(
prefs_js_path: &Path,
warnings: &mut Vec<String>,
) -> Result<Vec<PrefEntry>> {
if !prefs_js_path.exists() {
warnings.push(format!("prefs.js not found at {}", prefs_js_path.display()));
return Err(Error::PrefFileNotFound {
file: prefs_js_path.display().to_string(),
});
}
parse_prefs_js_file(prefs_js_path)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{PrefType, PrefValue};
use std::fs::write;
use tempfile::TempDir;
#[test]
fn test_merge_config_default() {
let config = MergeConfig::default();
assert!(config.include_builtins);
assert!(config.include_globals);
assert!(config.include_user);
assert!(config.continue_on_error);
}
#[test]
fn test_get_effective_pref() {
let prefs = vec![PrefEntry {
key: "test.pref".to_string(),
value: PrefValue::Bool(true),
pref_type: PrefType::User,
explanation: None,
source: Some(PrefSource::User),
source_file: Some("prefs.js".to_string()),
locked: None,
}];
assert!(get_effective_pref(&prefs, "test.pref").is_some());
assert!(get_effective_pref(&prefs, "nonexistent").is_none());
}
#[test]
fn test_load_user_preferences() {
let temp_dir = TempDir::new().unwrap();
let prefs_path = temp_dir.path().join("prefs.js");
let content = r#"
user_pref("test.pref", true);
user_pref("another.pref", "value");
"#;
write(&prefs_path, content).unwrap();
let mut warnings = Vec::new();
let prefs = load_user_preferences(&prefs_path, &mut warnings).unwrap();
assert_eq!(prefs.len(), 2);
assert!(warnings.is_empty());
}
#[test]
fn test_load_user_preferences_not_found() {
let temp_dir = TempDir::new().unwrap();
let prefs_path = temp_dir.path().join("nonexistent.js");
let mut warnings = Vec::new();
let result = load_user_preferences(&prefs_path, &mut warnings);
assert!(result.is_err());
assert!(!warnings.is_empty());
}
}