use std::path::{Path, PathBuf};
use eyre::{Result, WrapErr};
use indexmap::IndexMap;
use serde::Deserialize;
use crate::config::config_file::mise_toml::MiseToml;
use crate::file::display_path;
#[derive(Debug, Default, Clone, Deserialize)]
pub(crate) struct HistoryTomlConfig {
#[serde(default)]
pub exclude: Vec<String>,
#[serde(default)]
pub reload: IndexMap<String, String>,
#[serde(default)]
pub origin: Option<OriginTomlConfig>,
#[serde(default)]
pub encryption: Option<FileEncryptionConfig>,
}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub(crate) struct FileEncryptionConfig {
#[serde(default)]
pub recipients: Vec<String>,
}
pub(crate) fn file_recipients() -> Result<Vec<String>> {
let mut recipients = Vec::new();
for (path, layer) in layers()? {
if let Some(encryption) = layer.encryption {
if !crate::config::config_file::is_trusted(&path) {
eyre::bail!(
"trust the configuration before using its encryption recipients: {}",
display_path(&path)
);
}
recipients = encryption.recipients;
if recipients.is_empty() {
eyre::bail!(
"[history.encryption].recipients must not be empty; configure recipients before capturing encrypted files"
);
}
}
}
Ok(recipients)
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub(crate) struct OriginTomlConfig {
pub url: String,
#[serde(default = "default_branch")]
pub branch: String,
}
impl OriginTomlConfig {
pub(crate) fn plain(url: String, branch: String) -> Self {
Self { url, branch }
}
}
fn default_branch() -> String {
"main".to_string()
}
pub(crate) fn origin() -> Result<Option<(PathBuf, OriginTomlConfig)>> {
let mut found = None;
for (path, layer) in layers()? {
if let Some(origin) = layer.origin {
super::sync::network::validate_url(&origin.url)?;
found = Some((path, origin));
}
}
Ok(found)
}
pub(crate) fn layers() -> Result<Vec<(PathBuf, HistoryTomlConfig)>> {
let mut layers = vec![];
let files = crate::config::system_config_files()
.into_iter()
.chain(crate::config::global_config_files())
.filter(|path| path.is_file())
.filter(|path| path.extension().is_some_and(|ext| ext == "toml"));
for path in files {
if let Some(history) = read_layer(&path)? {
layers.push((path, history));
}
}
Ok(layers)
}
fn read_layer(path: &Path) -> Result<Option<HistoryTomlConfig>> {
let toml = MiseToml::from_file(path)
.wrap_err_with(|| format!("cannot read history configuration: {}", display_path(path)))?;
Ok(toml.history_config())
}
pub(crate) fn reload_commands() -> Result<IndexMap<String, String>> {
let mut commands = IndexMap::new();
for (path, layer) in layers()? {
if !crate::config::config_file::is_trusted(&path) {
if !layer.reload.is_empty() {
warn!(
"history: ignoring [history.reload] in untrusted {}",
display_path(&path)
);
}
continue;
}
for (glob, command) in &layer.reload {
commands.insert(glob.clone(), command.clone());
}
}
Ok(commands)
}
pub(crate) fn exclude_globs() -> Result<Vec<String>> {
let mut globs: Vec<String> = vec![];
for (_, layer) in layers()? {
globs.extend(layer.exclude.iter().cloned());
}
Ok(globs)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn malformed_encryption_is_rejected_and_layer_errors_propagate() {
let error = toml::from_str::<HistoryTomlConfig>("[encryption]\nrecipents = ['typo']\n")
.unwrap_err();
assert!(error.to_string().contains("recipents"));
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("config.toml");
std::fs::write(&path, "[history.encryption]\nrecipents = ['typo']\n").unwrap();
let error = read_layer(&path).unwrap_err();
assert!(format!("{error:#}").contains("config.toml"));
}
}