mod plist_utils;
use std::{
collections::HashMap,
path::{Path, PathBuf},
process::ExitStatus,
};
use color_eyre::eyre::{eyre, Context, Result};
use displaydoc::Display;
use log::{debug, error, trace, warn};
use serde_derive::{Deserialize, Serialize};
use thiserror::Error;
use crate::{
opts::{DefaultsReadOptions, DefaultsWriteOptions},
tasks::{
defaults::{
plist_utils::{get_plist_value_type, plist_path, write_defaults_values},
DefaultsError as E,
},
task::TaskStatus,
ResolveEnv,
},
};
impl ResolveEnv for DefaultsConfig {}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct DefaultsConfig(HashMap<String, HashMap<String, plist::Value>>);
pub(crate) fn run(config: DefaultsConfig, up_dir: &Path) -> Result<TaskStatus> {
if !(cfg!(target_os = "macos") || cfg!(target_os = "ios")) {
debug!("Defaults: skipping setting defaults as not on a Darwin platform.");
return Ok(TaskStatus::Skipped);
}
debug!("Setting defaults");
let (passed, errors): (Vec<_>, Vec<_>) = config
.0
.into_iter()
.map(|(domain, prefs)| write_defaults_values(&domain, prefs, up_dir))
.partition(Result::is_ok);
let errors: Vec<_> = errors.into_iter().map(Result::unwrap_err).collect();
let passed: Vec<_> = passed.into_iter().map(Result::unwrap).collect();
if passed.iter().all(|r| !r) && errors.is_empty() {
return Ok(TaskStatus::Skipped);
}
if passed.into_iter().any(|r| r) {
warn!("Defaults values have been changed, these may not take effect until you restart the system or run `sudo killall cfprefsd`");
}
if errors.is_empty() {
Ok(TaskStatus::Passed)
} else {
for error in &errors {
error!("{error:?}");
}
let mut errors_iter = errors.into_iter();
Err(errors_iter.next().ok_or(E::UnexpectedNone)?)
.with_context(|| eyre!("{:?}", errors_iter.collect::<Vec<_>>()))
}
}
#[derive(Error, Debug, Display)]
pub enum DefaultsError {
DeSerializationFailed {
domain: String,
key: String,
value: String,
source: serde_yaml::Error,
},
DefaultsCmd {
command: String,
stdout: String,
stderr: String,
status: ExitStatus,
},
DirCreation {
path: PathBuf,
source: std::io::Error,
},
ExpectedYamlString,
FileCopy {
from_path: PathBuf,
to_path: PathBuf,
source: std::io::Error,
},
FileRead {
path: PathBuf,
source: std::io::Error,
},
MissingHomeDir,
MissingKey { domain: String, key: String },
NotADictionary {
domain: String,
key: String,
plist_type: &'static str,
},
PlistRead { path: PathBuf, source: plist::Error },
PlistWrite { path: PathBuf, source: plist::Error },
SerializationFailed {
domain: String,
key: Option<String>,
source: serde_yaml::Error,
},
TooFewArgumentsWrite { domain: String, key: String },
TooManyArgumentsRead {
domain: Option<String>,
key: Option<String>,
},
MissingDomain {},
TooManyArgumentsWrite {
domain: String,
key: String,
value: Option<String>,
},
UnexpectedNumber { value: String },
UnexpectedPlistPath { path: PathBuf },
UnexpectedString {
value: Result<String, serde_yaml::Error>,
},
UnexpectedNone,
}
pub(crate) fn read(defaults_opts: DefaultsReadOptions) -> Result<(), E> {
let (domain, key) = if defaults_opts.global_domain {
if defaults_opts.key.is_some() {
return Err(E::TooManyArgumentsRead {
domain: defaults_opts.domain,
key: defaults_opts.key,
});
}
("NSGlobalDomain".to_owned(), defaults_opts.domain)
} else {
(
defaults_opts.domain.ok_or(E::MissingDomain {})?,
defaults_opts.key,
)
};
debug!("Domain: {domain:?}, Key: {key:?}");
let plist_path = plist_path(&domain)?;
debug!("Plist path: {plist_path:?}");
let plist: plist::Value = plist::from_file(&plist_path).map_err(|e| E::PlistRead {
path: plist_path,
source: e,
})?;
trace!("Plist: {plist:?}");
let value = match key.as_ref() {
Some(key) => plist
.as_dictionary()
.ok_or_else(|| E::NotADictionary {
domain: domain.clone(),
key: key.to_string(),
plist_type: get_plist_value_type(&plist),
})?
.get(key)
.ok_or_else(|| E::MissingKey {
domain: domain.clone(),
key: key.to_string(),
})?,
None => &plist,
};
print!(
"{}",
serde_yaml::to_string(value)
.map_err(|e| E::SerializationFailed {
domain,
key,
source: e
})?
.strip_prefix("---\n")
.ok_or(E::ExpectedYamlString {})?
);
Ok(())
}
pub(crate) fn write(defaults_opts: DefaultsWriteOptions, up_dir: &Path) -> Result<(), E> {
let (domain, key, value) = if defaults_opts.global_domain {
if defaults_opts.value.is_some() {
return Err(E::TooManyArgumentsWrite {
domain: defaults_opts.domain,
key: defaults_opts.key,
value: defaults_opts.value,
});
}
(
"NSGlobalDomain".to_owned(),
defaults_opts.domain,
defaults_opts.key,
)
} else if let Some(value) = defaults_opts.value {
(defaults_opts.domain, defaults_opts.key, value)
} else {
return Err(E::TooFewArgumentsWrite {
domain: defaults_opts.domain,
key: defaults_opts.key,
});
};
debug!("Domain: {domain:?}, Key: {key:?}, Value: {value:?}");
let mut prefs = HashMap::new();
let new_value: plist::Value =
serde_yaml::from_str(&value).map_err(|e| E::DeSerializationFailed {
domain: domain.clone(),
key: key.clone(),
value: value.clone(),
source: e,
})?;
trace!("Serialized Plist value: {new_value:?}");
prefs.insert(key, new_value);
write_defaults_values(&domain, prefs, up_dir)?;
Ok(())
}