pub mod age;
pub mod baseline;
pub mod conflict;
pub mod divergence;
pub mod gpg;
pub mod identity;
pub mod no_reverse;
pub mod pipeline;
pub mod reverse_merge;
pub mod template;
pub mod unarchive;
pub use pipeline::PreprocessMode;
use std::path::{Path, PathBuf};
use serde::Serialize;
use crate::fs::Fs;
use crate::Result;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum TransformType {
Generative,
Representational,
Opaque,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SecretLineRange {
pub start: usize,
pub end: usize,
pub reference: String,
}
#[derive(Debug, Clone, Default)]
pub struct ExpandedFile {
pub relative_path: PathBuf,
pub content: Vec<u8>,
pub is_dir: bool,
pub tracked_render: Option<String>,
pub context_hash: Option<[u8; 32]>,
pub secret_line_ranges: Vec<SecretLineRange>,
pub deploy_mode: Option<u32>,
}
pub trait Preprocessor: Send + Sync {
fn name(&self) -> &str;
fn transform_type(&self) -> TransformType;
fn matches_extension(&self, filename: &str) -> bool;
fn stripped_name(&self, filename: &str) -> String;
fn expand(&self, source: &Path, fs: &dyn Fs) -> Result<Vec<ExpandedFile>>;
fn supports_reverse_merge(&self) -> bool {
false
}
}
pub struct PreprocessorRegistry {
preprocessors: Vec<Box<dyn Preprocessor>>,
}
impl PreprocessorRegistry {
pub fn new() -> Self {
Self {
preprocessors: Vec::new(),
}
}
pub fn register(&mut self, preprocessor: Box<dyn Preprocessor>) {
self.preprocessors.push(preprocessor);
}
pub fn find_for_file(&self, filename: &str) -> Option<&dyn Preprocessor> {
self.preprocessors
.iter()
.find(|p| p.matches_extension(filename))
.map(|p| p.as_ref())
}
pub fn is_preprocessor_file(&self, filename: &str) -> bool {
self.find_for_file(filename).is_some()
}
pub fn is_empty(&self) -> bool {
self.preprocessors.is_empty()
}
pub fn len(&self) -> usize {
self.preprocessors.len()
}
}
impl Default for PreprocessorRegistry {
fn default() -> Self {
Self::new()
}
}
pub fn default_registry(
preprocessor_config: &crate::config::PreprocessorSection,
secret_config: &crate::config::SecretSection,
pather: &dyn crate::paths::Pather,
command_runner: std::sync::Arc<dyn crate::datastore::CommandRunner>,
) -> Result<(
PreprocessorRegistry,
Option<std::sync::Arc<crate::secret::SecretRegistry>>,
)> {
use std::sync::Arc;
let mut registry = PreprocessorRegistry::new();
registry.register(Box::new(unarchive::UnarchivePreprocessor::new()));
let template_config = &preprocessor_config.template;
let mut tpl = template::TemplatePreprocessor::new(
template_config.extensions.clone(),
template_config.vars.clone(),
pather,
)?;
let secret_registry = if secret_config.enabled {
build_secret_registry(
secret_config,
Arc::clone(&command_runner),
pather.dotfiles_root(),
)
} else {
None
};
if let Some(sr) = &secret_registry {
tpl = tpl.with_secret_registry(Arc::clone(sr));
}
registry.register(Box::new(tpl));
if preprocessor_config.age.enabled {
let identity_str = preprocessor_config.age.identity.trim();
let pp = if identity_str.is_empty() {
age::AgePreprocessor::from_env(Arc::clone(&command_runner))
} else {
age::AgePreprocessor::new(
Arc::clone(&command_runner),
std::path::PathBuf::from(identity_str),
preprocessor_config.age.extensions.clone(),
)
};
registry.register(Box::new(pp));
}
if preprocessor_config.gpg.enabled {
registry.register(Box::new(gpg::GpgPreprocessor::new(
Arc::clone(&command_runner),
preprocessor_config.gpg.extensions.clone(),
)));
}
Ok((registry, secret_registry))
}
pub fn build_secret_registry(
config: &crate::config::SecretSection,
runner: std::sync::Arc<dyn crate::datastore::CommandRunner>,
dotfiles_root: &std::path::Path,
) -> Option<std::sync::Arc<crate::secret::SecretRegistry>> {
use std::path::PathBuf;
use std::sync::Arc;
let mut reg = crate::secret::SecretRegistry::new();
let mut any_enabled = false;
if config.providers.pass.enabled {
let store_dir = if config.providers.pass.store_dir.is_empty() {
None
} else {
Some(PathBuf::from(&config.providers.pass.store_dir))
};
let provider = match store_dir {
Some(dir) => crate::secret::PassProvider::new(Arc::clone(&runner), dir),
None => crate::secret::PassProvider::from_env(Arc::clone(&runner)),
};
reg.register(Arc::new(provider));
any_enabled = true;
}
if config.providers.op.enabled {
let provider = crate::secret::OpProvider::from_env(Arc::clone(&runner));
reg.register(Arc::new(provider));
any_enabled = true;
}
if config.providers.bw.enabled {
let provider = crate::secret::BwProvider::from_env(Arc::clone(&runner));
reg.register(Arc::new(provider));
any_enabled = true;
}
if config.providers.sops.enabled {
let provider =
crate::secret::SopsProvider::new(Arc::clone(&runner), dotfiles_root.to_path_buf());
reg.register(Arc::new(provider));
any_enabled = true;
}
if config.providers.keychain.enabled {
let provider = crate::secret::KeychainProvider::from_env(Arc::clone(&runner));
reg.register(Arc::new(provider));
any_enabled = true;
}
if config.providers.secret_tool.enabled {
let provider = crate::secret::SecretToolProvider::from_env(Arc::clone(&runner));
reg.register(Arc::new(provider));
any_enabled = true;
}
if any_enabled {
Some(Arc::new(reg))
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(dead_code)]
fn assert_object_safe(_: &dyn Preprocessor) {}
#[allow(dead_code)]
fn assert_boxable(_: Box<dyn Preprocessor>) {}
#[test]
fn transform_type_eq() {
assert_eq!(TransformType::Generative, TransformType::Generative);
assert_ne!(TransformType::Generative, TransformType::Opaque);
}
#[test]
fn empty_registry() {
let registry = PreprocessorRegistry::new();
assert!(registry.is_empty());
assert_eq!(registry.len(), 0);
assert!(!registry.is_preprocessor_file("anything.txt"));
assert!(registry.find_for_file("anything.txt").is_none());
}
#[test]
fn registry_finds_preprocessor() {
let mut registry = PreprocessorRegistry::new();
registry.register(Box::new(
crate::preprocessing::identity::IdentityPreprocessor::new(),
));
assert!(!registry.is_empty());
assert_eq!(registry.len(), 1);
assert!(registry.is_preprocessor_file("config.toml.identity"));
assert!(!registry.is_preprocessor_file("config.toml"));
let found = registry.find_for_file("config.toml.identity").unwrap();
assert_eq!(found.name(), "identity");
}
#[test]
fn registry_first_match_wins() {
let mut registry = PreprocessorRegistry::new();
registry.register(Box::new(
crate::preprocessing::identity::IdentityPreprocessor::new(),
));
registry.register(Box::new(
crate::preprocessing::identity::IdentityPreprocessor::with_extension("identity"),
));
let found = registry.find_for_file("test.identity").unwrap();
assert_eq!(found.name(), "identity");
}
#[test]
fn registry_multiple_different_preprocessors() {
let mut registry = PreprocessorRegistry::new();
registry.register(Box::new(
crate::preprocessing::identity::IdentityPreprocessor::new(),
));
registry.register(Box::new(
crate::preprocessing::unarchive::UnarchivePreprocessor::new(),
));
assert_eq!(registry.len(), 2);
assert!(registry.is_preprocessor_file("config.toml.identity"));
assert!(registry.is_preprocessor_file("bin.tar.gz"));
let identity = registry.find_for_file("config.toml.identity").unwrap();
assert_eq!(identity.name(), "identity");
let unarchive = registry.find_for_file("bin.tar.gz").unwrap();
assert_eq!(unarchive.name(), "unarchive");
assert!(registry.find_for_file("regular.txt").is_none());
}
struct NoopRunner;
impl crate::datastore::CommandRunner for NoopRunner {
fn run(&self, _: &str, _: &[String]) -> Result<crate::datastore::CommandOutput> {
unreachable!("default_registry tests do not invoke runners")
}
}
fn make_default_registry(
preprocessor: crate::config::PreprocessorSection,
) -> PreprocessorRegistry {
let env = crate::testing::TempEnvironment::builder().build();
let secret = crate::config::SecretSection {
enabled: false,
providers: crate::config::SecretProvidersSection {
pass: crate::config::SecretProviderPass {
enabled: false,
store_dir: String::new(),
},
op: crate::config::SecretProviderOp { enabled: false },
bw: crate::config::SecretProviderBw { enabled: false },
sops: crate::config::SecretProviderSops { enabled: false },
keychain: crate::config::SecretProviderKeychain { enabled: false },
secret_tool: crate::config::SecretProviderSecretTool { enabled: false },
},
};
let runner: std::sync::Arc<dyn crate::datastore::CommandRunner> =
std::sync::Arc::new(NoopRunner);
let (reg, _) =
default_registry(&preprocessor, &secret, env.paths.as_ref(), runner).unwrap();
reg
}
fn empty_preprocessor_section() -> crate::config::PreprocessorSection {
crate::config::PreprocessorSection {
enabled: true,
template: crate::config::PreprocessorTemplateSection {
extensions: vec!["tmpl".into()],
vars: Default::default(),
no_reverse: Vec::new(),
},
age: crate::config::PreprocessorAgeSection {
enabled: false,
extensions: vec!["age".into()],
identity: String::new(),
},
gpg: crate::config::PreprocessorGpgSection {
enabled: false,
extensions: vec!["gpg".into(), "asc".into()],
},
}
}
#[test]
fn default_registry_does_not_register_age_or_gpg_when_disabled() {
let reg = make_default_registry(empty_preprocessor_section());
assert!(reg.find_for_file("id_ed25519.age").is_none());
assert!(reg.find_for_file("Brewfile.gpg").is_none());
assert!(reg.find_for_file("notes.asc").is_none());
assert!(reg.find_for_file("config.toml.tmpl").is_some());
assert!(reg.find_for_file("bin.tar.gz").is_some());
}
#[test]
fn default_registry_registers_age_when_enabled() {
let mut pre = empty_preprocessor_section();
pre.age.enabled = true;
pre.age.identity = "/k/id.txt".into();
let reg = make_default_registry(pre);
let pp = reg.find_for_file("id_ed25519.age").unwrap();
assert_eq!(pp.name(), "age");
}
#[test]
fn default_registry_registers_gpg_when_enabled_for_both_extensions() {
let mut pre = empty_preprocessor_section();
pre.gpg.enabled = true;
let reg = make_default_registry(pre);
let gpg_pp = reg.find_for_file("Brewfile.gpg").unwrap();
assert_eq!(gpg_pp.name(), "gpg");
let asc_pp = reg.find_for_file("notes.txt.asc").unwrap();
assert_eq!(asc_pp.name(), "gpg");
}
#[test]
fn registry_does_not_match_partial_extension() {
let mut registry = PreprocessorRegistry::new();
registry.register(Box::new(
crate::preprocessing::identity::IdentityPreprocessor::new(),
));
assert!(!registry.is_preprocessor_file("identity"));
assert!(!registry.is_preprocessor_file("fileidentity"));
}
}