1use std::collections::HashSet;
4use std::path::{Path, PathBuf};
5
6use secrecy::SecretString;
7use serde::{Deserialize, Serialize};
8
9use crate::errors::Result;
10use crate::output::Printer;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct PackageInfo {
16 pub name: String,
17 pub version: String,
18}
19
20pub trait PackageManager: Send + Sync {
21 fn name(&self) -> &str;
22 fn is_available(&self) -> bool;
23 fn can_bootstrap(&self) -> bool;
24 fn bootstrap(&self, printer: &Printer) -> Result<()>;
25 fn installed_packages(&self) -> Result<HashSet<String>>;
26 fn install(&self, packages: &[String], printer: &Printer) -> Result<()>;
27 fn uninstall(&self, packages: &[String], printer: &Printer) -> Result<()>;
28 fn update(&self, printer: &Printer) -> Result<()>;
29
30 fn available_version(&self, package: &str) -> Result<Option<String>>;
33
34 fn path_dirs(&self) -> Vec<String> {
37 Vec::new()
38 }
39
40 fn installed_packages_with_versions(&self) -> Result<Vec<PackageInfo>> {
43 Ok(self
44 .installed_packages()?
45 .into_iter()
46 .map(|name| PackageInfo {
47 name,
48 version: "unknown".into(),
49 })
50 .collect())
51 }
52
53 fn package_aliases(&self, _canonical_name: &str) -> Result<Vec<String>> {
56 Ok(vec![])
57 }
58}
59
60pub struct SystemDrift {
63 pub key: String,
64 pub expected: String,
65 pub actual: String,
66}
67
68pub trait SystemConfigurator: Send + Sync {
69 fn name(&self) -> &str;
71 fn is_available(&self) -> bool;
72
73 fn current_state(&self) -> Result<serde_yaml::Value>;
75
76 fn diff(&self, desired: &serde_yaml::Value) -> Result<Vec<SystemDrift>>;
78
79 fn apply(&self, desired: &serde_yaml::Value, printer: &Printer) -> Result<()>;
81}
82
83use std::collections::BTreeMap;
86
87#[derive(Debug)]
88pub struct FileLayer {
89 pub source_dir: PathBuf,
90 pub origin_source: String,
91 pub priority: u32,
92}
93
94#[derive(Debug)]
95pub struct FileTree {
96 pub files: BTreeMap<PathBuf, FileEntry>,
97}
98
99#[derive(Debug)]
100pub struct FileEntry {
101 pub content_hash: String,
102 pub permissions: Option<u32>,
103 pub is_template: bool,
104 pub source_path: PathBuf,
105 pub origin_source: String,
106}
107
108#[derive(Debug)]
109pub struct FileDiff {
110 pub target: PathBuf,
111 pub kind: FileDiffKind,
112}
113
114#[derive(Debug)]
115pub enum FileDiffKind {
116 Created { source: PathBuf },
117 Modified { source: PathBuf, diff: String },
118 Deleted,
119 PermissionsChanged { current: u32, desired: u32 },
120 Unchanged,
121}
122
123#[derive(Debug, Serialize)]
124pub enum FileAction {
125 Create {
126 source: PathBuf,
127 target: PathBuf,
128 origin: String,
129 strategy: crate::config::FileStrategy,
130 source_hash: Option<String>,
132 },
133 Update {
134 source: PathBuf,
135 target: PathBuf,
136 diff: String,
137 origin: String,
138 strategy: crate::config::FileStrategy,
139 source_hash: Option<String>,
141 },
142 Delete {
143 target: PathBuf,
144 origin: String,
145 },
146 SetPermissions {
147 target: PathBuf,
148 mode: u32,
149 origin: String,
150 },
151 Skip {
152 target: PathBuf,
153 reason: String,
154 origin: String,
155 },
156}
157
158pub trait FileManager: Send + Sync {
159 fn scan_source(&self, layers: &[FileLayer]) -> Result<FileTree>;
160 fn scan_target(&self, paths: &[PathBuf]) -> Result<FileTree>;
161 fn diff(&self, source: &FileTree, target: &FileTree) -> Result<Vec<FileDiff>>;
162 fn apply(&self, actions: &[FileAction], printer: &Printer) -> Result<()>;
163}
164
165#[derive(Debug, Serialize)]
168pub enum PackageAction {
169 Bootstrap {
170 manager: String,
171 method: String,
172 origin: String,
173 },
174 Install {
175 manager: String,
176 packages: Vec<String>,
177 origin: String,
178 },
179 Uninstall {
180 manager: String,
181 packages: Vec<String>,
182 origin: String,
183 },
184 Skip {
185 manager: String,
186 reason: String,
187 origin: String,
188 },
189}
190
191pub trait SecretBackend: Send + Sync {
194 fn name(&self) -> &str;
195 fn is_available(&self) -> bool;
196 fn encrypt_file(&self, path: &Path) -> Result<()>;
197 fn decrypt_file(&self, path: &Path) -> Result<SecretString>;
198 fn edit_file(&self, path: &Path) -> Result<()>;
199}
200
201pub trait SecretProvider: Send + Sync {
204 fn name(&self) -> &str;
205 fn is_available(&self) -> bool;
206 fn resolve(&self, reference: &str) -> Result<SecretString>;
207}
208
209#[derive(Debug, Serialize)]
212pub enum SecretAction {
213 Decrypt {
214 source: PathBuf,
215 target: PathBuf,
216 backend: String,
217 origin: String,
218 },
219 Resolve {
220 provider: String,
221 reference: String,
222 target: PathBuf,
223 origin: String,
224 },
225 ResolveEnv {
228 provider: String,
229 reference: String,
230 envs: Vec<String>,
231 origin: String,
232 },
233 Skip {
234 source: String,
235 reason: String,
236 origin: String,
237 },
238}
239
240pub struct ProviderRegistry {
243 pub package_managers: Vec<Box<dyn PackageManager>>,
244 pub system_configurators: Vec<Box<dyn SystemConfigurator>>,
245 pub file_manager: Option<Box<dyn FileManager>>,
246 pub secret_backend: Option<Box<dyn SecretBackend>>,
247 pub secret_providers: Vec<Box<dyn SecretProvider>>,
248 pub default_file_strategy: crate::config::FileStrategy,
249}
250
251impl ProviderRegistry {
252 pub fn new() -> Self {
253 Self {
254 package_managers: Vec::new(),
255 system_configurators: Vec::new(),
256 file_manager: None,
257 secret_backend: None,
258 secret_providers: Vec::new(),
259 default_file_strategy: crate::config::FileStrategy::Symlink,
260 }
261 }
262
263 pub fn available_package_managers(&self) -> Vec<&dyn PackageManager> {
264 self.package_managers
265 .iter()
266 .filter(|pm| pm.is_available())
267 .map(|pm| pm.as_ref())
268 .collect()
269 }
270
271 pub fn available_system_configurators(&self) -> Vec<&dyn SystemConfigurator> {
272 self.system_configurators
273 .iter()
274 .filter(|sc| sc.is_available())
275 .map(|sc| sc.as_ref())
276 .collect()
277 }
278}
279
280impl Default for ProviderRegistry {
281 fn default() -> Self {
282 Self::new()
283 }
284}
285
286pub fn parse_secret_reference(source: &str) -> Option<(&str, &str)> {
295 if let Some(rest) = source.strip_prefix("1password://") {
296 Some(("1password", rest))
297 } else if let Some(rest) = source.strip_prefix("bitwarden://") {
298 Some(("bitwarden", rest))
299 } else if let Some(rest) = source.strip_prefix("lastpass://") {
300 Some(("lastpass", rest))
301 } else if let Some(rest) = source.strip_prefix("vault://") {
302 Some(("vault", rest))
303 } else {
304 None
305 }
306}
307
308#[cfg(test)]
310pub(crate) struct StubPackageManager {
311 pub name: String,
312 pub available: bool,
313 pub installed: HashSet<String>,
314 pub versions: std::collections::HashMap<String, String>,
315 pub bootstrap_capable: bool,
316 pub installed_error: Option<String>,
319}
320
321#[cfg(test)]
322impl StubPackageManager {
323 pub fn new(name: &str) -> Self {
324 Self {
325 name: name.to_string(),
326 available: true,
327 installed: HashSet::new(),
328 versions: std::collections::HashMap::new(),
329 bootstrap_capable: false,
330 installed_error: None,
331 }
332 }
333
334 pub fn unavailable(mut self) -> Self {
335 self.available = false;
336 self
337 }
338
339 pub fn bootstrappable(mut self) -> Self {
340 self.bootstrap_capable = true;
341 self
342 }
343
344 pub fn with_installed(mut self, pkgs: &[&str]) -> Self {
345 for p in pkgs {
346 self.installed.insert((*p).to_string());
347 }
348 self
349 }
350
351 pub fn with_installed_error(mut self, message: &str) -> Self {
352 self.installed_error = Some(message.to_string());
353 self
354 }
355
356 pub fn with_package(mut self, pkg: &str, ver: &str) -> Self {
357 self.versions.insert(pkg.to_string(), ver.to_string());
358 self
359 }
360}
361
362#[cfg(test)]
363impl PackageManager for StubPackageManager {
364 fn name(&self) -> &str {
365 &self.name
366 }
367 fn is_available(&self) -> bool {
368 self.available
369 }
370 fn can_bootstrap(&self) -> bool {
371 self.bootstrap_capable
372 }
373 fn bootstrap(&self, _printer: &Printer) -> Result<()> {
374 Ok(())
375 }
376 fn installed_packages(&self) -> Result<HashSet<String>> {
377 if let Some(ref msg) = self.installed_error {
378 return Err(crate::errors::CfgdError::Io(std::io::Error::other(
379 msg.clone(),
380 )));
381 }
382 Ok(self.installed.clone())
383 }
384 fn install(&self, _packages: &[String], _printer: &Printer) -> Result<()> {
385 Ok(())
386 }
387 fn uninstall(&self, _packages: &[String], _printer: &Printer) -> Result<()> {
388 Ok(())
389 }
390 fn update(&self, _printer: &Printer) -> Result<()> {
391 Ok(())
392 }
393 fn available_version(&self, package: &str) -> Result<Option<String>> {
394 Ok(self.versions.get(package).cloned())
395 }
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401
402 #[test]
403 fn registry_filters_available_managers() {
404 let mut registry = ProviderRegistry::new();
405 registry
406 .package_managers
407 .push(Box::new(StubPackageManager::new("mock")));
408 registry
409 .package_managers
410 .push(Box::new(StubPackageManager::new("mock2").unavailable()));
411
412 let available = registry.available_package_managers();
413 assert_eq!(available.len(), 1);
414 assert_eq!(available[0].name(), "mock");
415 }
416
417 #[test]
418 fn empty_registry() {
419 let registry = ProviderRegistry::new();
420 assert!(registry.available_package_managers().is_empty());
421 assert!(registry.available_system_configurators().is_empty());
422 assert!(registry.file_manager.is_none());
423 assert!(registry.secret_backend.is_none());
424 }
425
426 #[test]
427 fn test_default_installed_packages_with_versions_empty() {
428 let mock = StubPackageManager::new("mock");
429 let pkgs = mock.installed_packages_with_versions().unwrap();
430 assert!(pkgs.is_empty());
431 }
432
433 #[test]
434 fn test_default_package_aliases_empty() {
435 let mock = StubPackageManager::new("mock");
436 let aliases = mock.package_aliases("fd").unwrap();
437 assert!(aliases.is_empty());
438 }
439
440 #[test]
441 fn parse_secret_reference_1password() {
442 let (provider, rest) = parse_secret_reference("1password://Vault/Item/Field").unwrap();
443 assert_eq!(provider, "1password");
444 assert_eq!(rest, "Vault/Item/Field");
445 }
446
447 #[test]
448 fn parse_secret_reference_bitwarden() {
449 let (provider, rest) = parse_secret_reference("bitwarden://folder/item").unwrap();
450 assert_eq!(provider, "bitwarden");
451 assert_eq!(rest, "folder/item");
452 }
453
454 #[test]
455 fn parse_secret_reference_lastpass() {
456 let (provider, rest) = parse_secret_reference("lastpass://folder/item/field").unwrap();
457 assert_eq!(provider, "lastpass");
458 assert_eq!(rest, "folder/item/field");
459 }
460
461 #[test]
462 fn parse_secret_reference_vault() {
463 let (provider, rest) = parse_secret_reference("vault://secret/path#field").unwrap();
464 assert_eq!(provider, "vault");
465 assert_eq!(rest, "secret/path#field");
466 }
467
468 #[test]
469 fn parse_secret_reference_unknown_returns_none() {
470 assert!(parse_secret_reference("plaintext").is_none());
471 assert!(parse_secret_reference("file:///etc/passwd").is_none());
472 assert!(parse_secret_reference("").is_none());
473 }
474
475 #[test]
476 fn provider_registry_default_matches_new() {
477 let reg = ProviderRegistry::default();
478 assert!(reg.package_managers.is_empty());
479 assert!(reg.system_configurators.is_empty());
480 assert!(reg.file_manager.is_none());
481 assert!(reg.secret_backend.is_none());
482 assert!(reg.secret_providers.is_empty());
483 }
484
485 struct StubConfigurator {
486 name: String,
487 available: bool,
488 }
489
490 impl SystemConfigurator for StubConfigurator {
491 fn name(&self) -> &str {
492 &self.name
493 }
494 fn is_available(&self) -> bool {
495 self.available
496 }
497 fn current_state(&self) -> Result<serde_yaml::Value> {
498 Ok(serde_yaml::Value::Null)
499 }
500 fn diff(&self, _desired: &serde_yaml::Value) -> Result<Vec<SystemDrift>> {
501 Ok(Vec::new())
502 }
503 fn apply(&self, _desired: &serde_yaml::Value, _printer: &Printer) -> Result<()> {
504 Ok(())
505 }
506 }
507
508 #[test]
509 fn available_system_configurators_filters_unavailable() {
510 let mut reg = ProviderRegistry::new();
511 reg.system_configurators.push(Box::new(StubConfigurator {
512 name: "shell".to_string(),
513 available: true,
514 }));
515 reg.system_configurators.push(Box::new(StubConfigurator {
516 name: "systemd".to_string(),
517 available: false,
518 }));
519
520 let available = reg.available_system_configurators();
521 assert_eq!(available.len(), 1);
522 assert_eq!(available[0].name(), "shell");
523 }
524
525 #[test]
526 fn stub_builder_chain_full() {
527 let stub = StubPackageManager::new("brew")
528 .bootstrappable()
529 .with_installed(&["jq", "ripgrep"])
530 .with_package("jq", "1.7.1");
531 assert!(stub.is_available());
532 assert!(stub.can_bootstrap());
533 assert_eq!(stub.installed_packages().unwrap().len(), 2);
534 assert_eq!(
535 stub.available_version("jq").unwrap(),
536 Some("1.7.1".to_string())
537 );
538 assert!(stub.available_version("missing").unwrap().is_none());
539 }
540
541 #[test]
542 fn stub_with_installed_error_returns_err() {
543 let stub =
544 StubPackageManager::new("brew").with_installed_error("simulated brew list failure");
545 let err = stub.installed_packages().unwrap_err();
546 let msg = format!("{err}");
547 assert!(
548 msg.contains("simulated brew list failure"),
549 "unexpected error message: {msg}"
550 );
551 }
552
553 #[test]
554 fn stub_default_installed_packages_with_versions_with_content() {
555 let stub = StubPackageManager::new("brew").with_installed(&["fd", "jq"]);
556 let mut pkgs = stub.installed_packages_with_versions().unwrap();
557 pkgs.sort_by(|a, b| a.name.cmp(&b.name));
558 assert_eq!(pkgs.len(), 2);
559 assert_eq!(pkgs[0].name, "fd");
560 assert_eq!(pkgs[0].version, "unknown");
561 assert_eq!(pkgs[1].name, "jq");
562 assert_eq!(pkgs[1].version, "unknown");
563 }
564
565 #[test]
566 fn stub_default_path_dirs_empty() {
567 let stub = StubPackageManager::new("apt");
568 assert!(stub.path_dirs().is_empty());
569 }
570
571 #[test]
572 fn package_info_serde_round_trips() {
573 let info = PackageInfo {
574 name: "jq".to_string(),
575 version: "1.7.1".to_string(),
576 };
577 let json = serde_json::to_string(&info).unwrap();
578 assert!(json.contains("\"name\":\"jq\""));
579 assert!(json.contains("\"version\":\"1.7.1\""));
580 let parsed: PackageInfo = serde_json::from_str(&json).unwrap();
581 assert_eq!(parsed.name, info.name);
582 assert_eq!(parsed.version, info.version);
583 }
584}