#![allow(unused_assignments)]
mod plist_utils;
mod ser;
use crate::opts::DefaultsReadOptions;
use crate::opts::DefaultsWriteOptions;
use crate::tasks::ResolveEnv;
use crate::tasks::TaskError;
use crate::tasks::defaults::DefaultsError as E;
use crate::tasks::defaults::plist_utils::get_plist_value_type;
use crate::tasks::defaults::plist_utils::plist_path;
use crate::tasks::defaults::plist_utils::write_defaults_values;
use crate::tasks::defaults::ser::replace_data_in_plist;
use crate::tasks::task::TaskStatus;
use camino::Utf8Path;
use camino::Utf8PathBuf;
use color_eyre::eyre::Context;
use color_eyre::eyre::Result;
use color_eyre::eyre::eyre;
use displaydoc::Display;
use itertools::Itertools;
use serde_derive::Deserialize;
use serde_derive::Serialize;
use std::collections::HashMap;
use std::io::Read;
use std::process::ExitStatus;
use thiserror::Error;
use tracing::debug;
use tracing::error;
use tracing::trace;
use tracing::warn;
impl ResolveEnv for DefaultsConfig {
fn resolve_env<F>(&mut self, env_fn: F) -> Result<(), TaskError>
where
F: Fn(&str) -> Result<String, TaskError>,
{
let keys = self.0.keys().cloned().collect_vec();
for domain in keys {
let replaced_domain = env_fn(&domain)?;
if replaced_domain == domain {
continue;
}
let pref = self
.0
.remove(&domain)
.ok_or_else(|| {
eyre!("Expected to find the domain in the prefs mapping as we just checked it.")
})
.map_err(|e| TaskError::EyreError { source: e })?;
_ = self.0.insert(replaced_domain, pref);
}
Ok(())
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct DefaultsConfig(HashMap<String, HashMap<String, plist::Value>>);
pub(crate) fn run(config: DefaultsConfig, up_dir: &Utf8Path) -> 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, false, 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)?)
.wrap_err_with(|| eyre!("{:?}", errors_iter.collect::<Vec<_>>()))
}
}
#[allow(clippy::doc_markdown)]
#[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: Utf8PathBuf,
source: std::io::Error,
},
FileCopy {
from_path: Utf8PathBuf,
to_path: Utf8PathBuf,
source: std::io::Error,
},
FileRead {
path: Utf8PathBuf,
source: std::io::Error,
},
MissingHomeDir {
source: color_eyre::Report,
},
MissingKey {
domain: String,
key: String,
},
NotADictionary {
domain: String,
key: String,
plist_type: &'static str,
},
PlistRead {
path: Utf8PathBuf,
source: plist::Error,
},
PlistWrite {
path: Utf8PathBuf,
source: plist::Error,
},
PlistSudoWrite {
path: Utf8PathBuf,
source: std::io::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: Utf8PathBuf,
},
UnexpectedString {
value: Result<String, serde_yaml::Error>,
},
UnexpectedNone,
EyreError {
source: color_eyre::Report,
},
}
pub(crate) fn read(current_host: bool, 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, current_host)?;
debug!("Plist path: {plist_path}");
let plist: plist::Value = if &plist_path == "-" {
let mut bytes = Vec::new();
_ = std::io::stdin()
.read_to_end(&mut bytes)
.map_err(|e| E::FileRead {
path: plist_path.clone(),
source: e,
})?;
plist::from_bytes(&bytes)
} else {
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.clone(),
plist_type: get_plist_value_type(&plist),
})?
.get(key)
.ok_or_else(|| E::MissingKey {
domain: domain.clone(),
key: key.clone(),
})?,
None => &plist,
};
let serialization_result = serde_yaml::to_string(value);
let serialized_string = if let Ok(s) = serialization_result {
s
} else {
warn!(
"Serializing plist value to YAML failed, assuming this is because it contained binary \
data and replacing that with hex-encoded binary data. This is incorrect, but allows \
the output to be printed."
);
let mut value = value.clone();
replace_data_in_plist(&mut value).map_err(|e| E::EyreError { source: e })?;
serde_yaml::to_string(&value).map_err(|e| E::SerializationFailed {
domain,
key,
source: e,
})?
};
print!("{serialized_string}");
Ok(())
}
pub(crate) fn write(
current_host: bool,
defaults_opts: DefaultsWriteOptions,
up_dir: &Utf8Path,
) -> 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, current_host, up_dir)?;
Ok(())
}