use crate::cli;
use ffcv::PrefValue;
use ffcv::PrefValueExt;
use ffcv::{
find_all_firefox_installations, find_firefox_installation, find_profile_path,
list_profiles as list_profiles_impl, merge_all_preferences, query_preferences, MergeConfig,
PrefSource,
};
pub struct ViewConfigParams<'a> {
pub stdin: bool,
pub profile_name: &'a str,
pub profiles_dir_opt: Option<&'a std::path::Path>,
pub install_dir_opt: Option<&'a std::path::Path>,
pub max_file_size: usize,
pub query_patterns: &'a [&'a str],
pub get: Option<String>,
pub output_type: cli::OutputType,
pub show_only_modified: bool,
pub all: bool,
pub unexplained_only: bool,
}
pub fn list_profiles(
profiles_dir_opt: Option<&std::path::Path>,
) -> Result<(), Box<dyn std::error::Error>> {
let profiles = list_profiles_impl(profiles_dir_opt).map_err(|e| {
anyhow::anyhow!(
"Failed to list profiles: {}. Make sure Firefox is installed.",
e
)
})?;
let json = serde_json::to_string_pretty(&profiles)?;
println!("{}", json);
Ok(())
}
pub fn list_installations(all: bool) -> Result<(), Box<dyn std::error::Error>> {
let installations = if all {
find_all_firefox_installations()
.map_err(|e| anyhow::anyhow!("Failed to find Firefox installations: {}", e))?
} else {
match find_firefox_installation()
.map_err(|e| anyhow::anyhow!("Failed to find Firefox installation: {}", e))?
{
Some(install) => vec![install],
None => {
println!("[]");
return Ok(());
}
}
};
let json = serde_json::to_string_pretty(&installations)?;
println!("{}", json);
Ok(())
}
fn read_stdin_content(max_file_size: usize) -> Result<String, Box<dyn std::error::Error>> {
use std::io::{self, Read};
let mut buffer = String::new();
let bytes_read = io::stdin().read_to_string(&mut buffer).map_err(|e| {
anyhow::anyhow!(
"Failed to read from stdin: {}. Make sure to pipe prefs.js content.",
e
)
})?;
if bytes_read > max_file_size {
return Err(anyhow::anyhow!(
"Input from stdin exceeds maximum size limit: {} bytes > {} bytes. \
Use --max-file-size to increase the limit.",
bytes_read,
max_file_size
)
.into());
}
Ok(buffer)
}
pub fn view_config(params: ViewConfigParams) -> Result<(), Box<dyn std::error::Error>> {
if params.stdin {
let content = read_stdin_content(params.max_file_size)?;
let preferences: Vec<ffcv::PrefEntry> = ffcv::parse_prefs_js(&content).map_err(|e| {
anyhow::anyhow!(
"Failed to parse preferences from stdin: {}. The input may be malformed.",
e
)
})?;
output_preferences(&preferences, ¶ms)?;
return Ok(());
}
let profile_path = find_profile_path(params.profile_name, params.profiles_dir_opt).map_err(|e| {
anyhow::anyhow!(
"Failed to find profile '{}': {}. Make sure Firefox is installed and the profile exists.\n\
Use 'ffcv profile' to see available profiles.",
params.profile_name,
e
)
})?;
let merge_config = MergeConfig {
include_builtins: params.all,
include_globals: params.all,
include_user: true,
continue_on_error: true,
};
let merged = merge_all_preferences(&profile_path, params.install_dir_opt, &merge_config)
.map_err(|e| anyhow::anyhow!("Failed to merge preferences: {}", e))?;
for warning in &merged.warnings {
eprintln!("Warning: {}", warning);
}
let preferences = merged.entries;
output_preferences(&preferences, ¶ms)?;
Ok(())
}
fn output_preferences(
preferences: &[ffcv::PrefEntry],
params: &ViewConfigParams,
) -> Result<(), Box<dyn std::error::Error>> {
let mut output_prefs = preferences.to_vec();
if let Some(ref get_key) = params.get {
if let Some(entry) = output_prefs.iter().find(|e| e.key == *get_key) {
if params.unexplained_only && entry.explanation.is_some() {
return Err(anyhow::anyhow!(
"Preference '{}' has an explanation, but --unexplained-only was specified",
get_key
)
.into());
}
output_raw_value(&entry.value)?;
return Ok(());
}
return Err(anyhow::anyhow!("Preference '{}' not found", get_key).into());
}
if params.show_only_modified {
output_prefs.retain(|entry| {
entry.source == Some(PrefSource::User)
});
}
if !params.query_patterns.is_empty() {
output_prefs = query_preferences(&output_prefs, params.query_patterns)
.map_err(|e| anyhow::anyhow!("Failed to apply query: {}", e))?;
}
if params.unexplained_only {
output_prefs.retain(|entry| {
entry.explanation.is_none()
});
}
let json = match params.output_type {
cli::OutputType::JsonObject => {
let json_map: std::collections::BTreeMap<String, serde_json::Value> = output_prefs
.iter()
.map(|entry| (entry.key.clone(), entry.value.to_json_value()))
.collect();
serde_json::to_string_pretty(&json_map)?
}
cli::OutputType::JsonArray => {
let mut sorted_entries = output_prefs.clone();
sorted_entries.sort_by(|a, b| a.key.cmp(&b.key));
serde_json::to_string_pretty(&sorted_entries)?
}
};
println!("{}", json);
Ok(())
}
fn output_raw_value(value: &PrefValue) -> Result<(), Box<dyn std::error::Error>> {
match value {
PrefValue::String(s) => println!("{}", s),
PrefValue::Bool(b) => println!("{}", b),
PrefValue::Integer(i) => println!("{}", i),
PrefValue::Float(f) => println!("{}", f),
PrefValue::Null => println!("null"),
}
Ok(())
}
#[cfg(test)]
mod tests {
use ffcv::PrefType;
use ffcv::PrefValue;
use ffcv::PrefValueExt;
fn format_value(value: &PrefValue) -> String {
match value {
PrefValue::Integer(i) => format!("{}", i),
PrefValue::Float(f) => format!("{}", f),
PrefValue::String(s) => s.clone(),
PrefValue::Bool(b) => format!("{}", b),
PrefValue::Null => "null".to_string(),
}
}
#[test]
fn test_pref_entry_serialization() {
let entry = ffcv::PrefEntry {
key: "test.key".to_string(),
value: PrefValue::String("test value".to_string()),
pref_type: PrefType::User,
explanation: None,
source: Some(ffcv::PrefSource::User),
source_file: Some("prefs.js".to_string()),
locked: None,
};
let json_str = serde_json::to_string(&entry).unwrap();
assert!(json_str.contains("\"pref_type\":\"user\""));
assert!(json_str.contains("\"key\":\"test.key\""));
assert!(json_str.contains("\"value\":{\"String\":\"test value\"}"));
assert!(!json_str.contains("explanation"));
}
#[test]
fn test_pref_type_serialization() {
let tests = vec![
(PrefType::User, "user"),
(PrefType::Default, "default"),
(PrefType::Locked, "locked"),
(PrefType::Sticky, "sticky"),
];
for (pref_type, expected_str) in tests {
let json_str = serde_json::to_string(&pref_type).unwrap();
assert_eq!(json_str, format!("\"{}\"", expected_str));
}
}
#[test]
fn test_json_array_output_with_types() {
let input = r#"
user_pref("user.pref", "value1");
pref("default.pref", "value2");
lock_pref("locked.pref", "value3");
sticky_pref("sticky.pref", "value4");
"#;
let mut array_output = ffcv::parse_prefs_js(input).unwrap();
array_output.sort_by(|a, b| a.key.cmp(&b.key));
let json_str = serde_json::to_string_pretty(&array_output).unwrap();
assert!(json_str.contains("pref_type"));
assert!(json_str.contains("\"user\""));
assert!(json_str.contains("\"default\""));
assert!(json_str.contains("\"locked\""));
assert!(json_str.contains("\"sticky\""));
assert!(json_str.contains("user.pref"));
assert!(json_str.contains("default.pref"));
assert!(json_str.contains("locked.pref"));
assert!(json_str.contains("sticky.pref"));
let parsed: Vec<serde_json::Value> = serde_json::from_str(&json_str).unwrap();
assert_eq!(parsed.len(), 4);
let keys: Vec<&str> = parsed
.iter()
.map(|entry| entry["key"].as_str().unwrap())
.collect();
assert_eq!(
keys,
vec!["default.pref", "locked.pref", "sticky.pref", "user.pref"]
);
for entry in parsed {
assert!(entry.is_object());
let obj = entry.as_object().unwrap();
assert!(obj.contains_key("key"));
assert!(obj.contains_key("value"));
assert!(obj.contains_key("pref_type"));
assert!(!obj.contains_key("explanation"));
}
}
#[test]
fn test_output_raw_value_integer() {
let value = PrefValue::Integer(3);
let output = format_value(&value);
assert_eq!(output, "3");
assert!(!output.contains('.'));
}
#[test]
fn test_output_raw_value_negative_integer() {
let value = PrefValue::Integer(-42);
let output = format_value(&value);
assert_eq!(output, "-42");
assert!(!output.contains('.'));
}
#[test]
fn test_output_raw_value_zero() {
let value = PrefValue::Integer(0);
let output = format_value(&value);
assert_eq!(output, "0");
assert!(!output.contains('.'));
}
#[test]
fn test_output_raw_value_float() {
let value = PrefValue::Float(2.5);
let output = format_value(&value);
assert_eq!(output, "2.5");
assert!(output.contains('.'));
}
#[test]
fn test_output_raw_value_float_whole_number() {
let value = PrefValue::Integer(3);
let output = format_value(&value);
assert_eq!(output, "3");
assert!(!output.contains('.'));
}
#[test]
fn test_output_raw_value_string() {
let value = PrefValue::String("test value".to_string());
let output = format_value(&value);
assert_eq!(output, "test value");
}
#[test]
fn test_output_raw_value_bool() {
let value = PrefValue::Bool(true);
let output = format_value(&value);
assert_eq!(output, "true");
let value = PrefValue::Bool(false);
let output = format_value(&value);
assert_eq!(output, "false");
}
#[test]
fn test_output_raw_value_null() {
let value = PrefValue::Null;
let output = format_value(&value);
assert_eq!(output, "null");
}
#[test]
fn test_pref_entry_serialization_with_explanation() {
let entry = ffcv::PrefEntry {
key: "javascript.enabled".to_string(),
value: PrefValue::Bool(true),
pref_type: PrefType::Default,
explanation: Some("Master switch to enable or disable JavaScript execution."),
source: Some(ffcv::PrefSource::User),
source_file: Some("prefs.js".to_string()),
locked: None,
};
let json_str = serde_json::to_string(&entry).unwrap();
assert!(json_str.contains("\"explanation\":"));
assert!(json_str.contains("Master switch to enable or disable JavaScript execution"));
}
#[test]
fn test_pref_entry_serialization_without_explanation() {
let entry = ffcv::PrefEntry {
key: "unknown.pref".to_string(),
value: PrefValue::String("test".to_string()),
pref_type: PrefType::User,
explanation: None,
source: Some(ffcv::PrefSource::User),
source_file: Some("prefs.js".to_string()),
locked: None,
};
let json_str = serde_json::to_string(&entry).unwrap();
assert!(!json_str.contains("explanation"));
}
#[test]
fn test_json_array_output_includes_explanations() {
let input = r#"
user_pref("javascript.enabled", true);
user_pref("browser.startup.homepage", "https://example.com");
"#;
let mut array_output = ffcv::parse_prefs_js(input).unwrap();
array_output.sort_by(|a, b| a.key.cmp(&b.key));
let json_str = serde_json::to_string_pretty(&array_output).unwrap();
assert!(json_str.contains("Master switch to enable or disable JavaScript"));
let parsed: Vec<serde_json::Value> = serde_json::from_str(&json_str).unwrap();
assert_eq!(parsed.len(), 2);
let js_entry = parsed
.iter()
.find(|entry| entry["key"] == "javascript.enabled")
.expect("javascript.enabled should be present")
.as_object()
.unwrap();
assert!(js_entry.contains_key("explanation"));
let homepage_entry = parsed
.iter()
.find(|entry| entry["key"] == "browser.startup.homepage")
.expect("browser.startup.homepage should be present")
.as_object()
.unwrap();
assert!(!homepage_entry.contains_key("explanation"));
}
#[test]
fn test_json_object_output_sorted_alphabetically() {
let input = r#"
user_pref("zebra.pref", "value1");
user_pref("apple.pref", "value2");
user_pref("banana.pref", "value3");
"#;
let prefs = ffcv::parse_prefs_js(input).unwrap();
let json_map: std::collections::BTreeMap<String, serde_json::Value> = prefs
.iter()
.map(|entry| (entry.key.clone(), entry.value.to_json_value()))
.collect();
let json_str = serde_json::to_string_pretty(&json_map).unwrap();
let parsed: serde_json::Map<String, serde_json::Value> =
serde_json::from_str(&json_str).unwrap();
let keys: Vec<&String> = parsed.keys().collect();
assert_eq!(keys, vec!["apple.pref", "banana.pref", "zebra.pref"]);
}
#[test]
fn test_stdin_size_limit_enforcement() {
let large_content = "user_pref(\"test\", \"x\");".repeat(1000);
let small_limit = 100;
assert!(large_content.len() > small_limit);
}
#[test]
fn test_max_file_size_parameter() {
let max_size: usize = 10_485_760; assert_eq!(max_size, 10_485_760);
let size_in_mb = max_size / 1_048_576;
assert_eq!(size_in_mb, 10);
}
}