mod convert;
pub mod types;
use anyhow::{Context, Result, bail};
use std::{
collections::{HashMap, HashSet},
fs::{self, File},
io::Cursor,
path::PathBuf,
};
use crate::{
Domain, FindMatch, PrefValue,
preferences::convert::{plist_to_prefvalue, prefvalue_to_plist},
};
use plist::Value;
use crate::core::foundation;
pub struct Preferences;
impl Preferences {
pub fn list_domains() -> Result<HashSet<Domain>> {
let list = foundation::list_domains()?;
let domains: HashSet<Domain> = list.iter().map(|f| Domain::User(f.to_string())).collect();
Ok(domains)
}
pub fn find(word: &str) -> Result<HashMap<Domain, Vec<FindMatch>>> {
let word_lower = word.to_lowercase();
let mut results: std::collections::HashMap<Domain, Vec<FindMatch>> =
std::collections::HashMap::new();
let domains: Vec<Domain> = Self::list_domains()?
.into_iter()
.chain([Domain::Global])
.collect();
for domain in domains {
let loaded = foundation::read_pref_domain(&domain.to_string())?;
let mut matches = Vec::new();
Self::find_in_value(&loaded, &word_lower, String::new(), &mut matches);
if !matches.is_empty() {
results.insert(domain, matches);
}
}
Ok(results)
}
fn find_in_value(
val: &PrefValue,
word_lower: &str,
key_path: String,
matches: &mut Vec<FindMatch>,
) {
fn contains_word(haystack: &str, needle: &str) -> bool {
haystack.to_lowercase().contains(needle)
}
match val {
PrefValue::Dictionary(dict) => {
for (k, v) in dict {
let new_key_path = if key_path.is_empty() {
k.clone()
} else {
format!("{key_path}.{k}")
};
if contains_word(k, word_lower) {
matches.push(FindMatch {
key: new_key_path.clone(),
value: v.clone(),
});
}
Self::find_in_value(v, word_lower, new_key_path, matches);
}
}
PrefValue::Array(arr) => {
for (i, v) in arr.iter().enumerate() {
let new_key_path = format!("{key_path}[{i}]");
Self::find_in_value(v, word_lower, new_key_path, matches);
}
}
_ => {
if contains_word(&val.to_string(), word_lower) {
matches.push(FindMatch {
key: key_path.clone(),
value: val.clone(),
});
}
}
}
}
pub fn read(domain: Domain, key: &str) -> Result<PrefValue> {
let cf_name = &domain.get_cf_name();
foundation::read_pref(cf_name, key)
}
pub fn read_domain(domain: Domain) -> Result<PrefValue> {
let cf_name = &domain.get_cf_name();
foundation::read_pref_domain(cf_name)
}
pub fn write(domain: Domain, key: &str, value: PrefValue) -> Result<()> {
let cf_name = &domain.get_cf_name();
foundation::write_pref(cf_name, key, &value)?;
Ok(())
}
pub fn delete(domain: Domain, key: &str) -> Result<()> {
let cf_name = &domain.get_cf_name();
foundation::delete_key(cf_name, key)
}
pub fn delete_domain(domain: Domain) -> Result<()> {
let cf_name = &domain.get_cf_name();
foundation::delete_domain(cf_name)
}
pub fn read_type(domain: Domain, key: &str) -> Result<String> {
let cf_name = domain.get_cf_name();
let loaded = foundation::read_pref(&cf_name, key)?;
Ok(loaded.get_type().to_string())
}
pub fn rename(domain: Domain, old_key: &str, new_key: &str) -> Result<()> {
let cf_name = &domain.get_cf_name();
let val = foundation::read_pref(cf_name, old_key)?;
foundation::write_pref(cf_name, new_key, &val)?;
foundation::delete_key(cf_name, old_key)?;
Ok(())
}
pub fn import(domain: Domain, import_path: &str) -> Result<()> {
let data = fs::read(import_path)?;
let plist_val = Value::from_reader(Cursor::new(&data))?;
let dict = match plist_val {
Value::Dictionary(d) => d,
_ => {
bail!("Import must be a dictionary at root.")
}
};
let cf_name = &domain.get_cf_name();
for (k, v) in dict {
let pv = plist_to_prefvalue(&v)?;
foundation::write_pref(cf_name, &k, &pv)?;
}
Ok(())
}
pub fn export(domain: Domain, export_path: &str) -> Result<()> {
let cf_name = &domain.get_cf_name();
let pref = foundation::read_pref_domain(cf_name)?;
if !matches!(pref, PrefValue::Dictionary(_)) {
bail!("CF export produced non-dictionary root")
}
let plist = prefvalue_to_plist(&pref);
let path = PathBuf::from(export_path);
let file = File::create(path)?;
plist
.to_writer_binary(file)
.context("failed to export CF domain to plist")?;
Ok(())
}
}