1pub mod deploy;
6pub mod diff;
7
8use std::borrow::Cow;
9use std::fmt;
10use std::io;
11use std::ops::Deref;
12use std::path::{Path, PathBuf};
13
14use crate::profile::link;
15use crate::profile::LayeredProfile;
16use crate::profile::{dotfile::Dotfile, source::PunktfSource};
17
18use color_eyre::eyre::Context;
19
20use crate::template::source::Source;
21use crate::template::Template;
22
23pub type Result = std::result::Result<(), Box<dyn std::error::Error>>;
25
26#[derive(Debug, Clone)]
28struct PathLink {
29 source: PathBuf,
31
32 target: PathBuf,
34}
35
36impl PathLink {
37 const fn new(source: PathBuf, target: PathBuf) -> Self {
39 Self { source, target }
40 }
41
42 fn join(mut self, relative: &Path) -> Self {
46 self.source = self.source.join(relative);
47 self.target = self.target.join(relative);
48
49 self
50 }
51}
52
53#[derive(Debug, Clone)]
55struct Paths {
56 root: PathLink,
60
61 child: Option<PathLink>,
66}
67
68impl Paths {
69 const fn new(root_source: PathBuf, root_target: PathBuf) -> Self {
71 Self {
72 root: PathLink::new(root_source, root_target),
73 child: None,
74 }
75 }
76
77 fn with_child(self, rel_path: impl Into<PathBuf>) -> Self {
79 let Paths { root, child } = self;
80 let rel_path = rel_path.into();
81
82 let child = if let Some(child) = child {
83 child.join(&rel_path)
84 } else {
85 PathLink::new(rel_path.clone(), rel_path)
86 };
87
88 Self {
89 root,
90 child: Some(child),
91 }
92 }
93
94 pub const fn is_root(&self) -> bool {
97 self.child.is_none()
98 }
99
100 pub const fn is_child(&self) -> bool {
103 self.child.is_some()
104 }
105
106 pub fn root_source_path(&self) -> &Path {
108 &self.root.source
109 }
110
111 pub fn root_target_path(&self) -> &Path {
113 &self.root.target
114 }
115
116 pub fn child_source_path(&self) -> Cow<'_, Path> {
120 if let Some(child) = &self.child {
121 Cow::Owned(self.root_source_path().join(&child.source))
122 } else {
123 Cow::Borrowed(self.root_source_path())
124 }
125 }
126
127 pub fn child_target_path(&self) -> Cow<'_, Path> {
131 if let Some(child) = &self.child {
132 Cow::Owned(self.root_target_path().join(&child.target))
133 } else {
134 Cow::Borrowed(self.root_target_path())
135 }
136 }
137}
138
139#[derive(Debug)]
141pub enum Kind<'a> {
142 Root(&'a Dotfile),
144
145 Child {
147 root: &'a Dotfile,
150
151 root_source_path: PathBuf,
153
154 root_target_path: PathBuf,
156 },
157}
158
159impl<'a> Kind<'a> {
160 fn from_paths(paths: Paths, dotfile: &'a Dotfile) -> Self {
162 if paths.is_root() {
163 Self::Root(dotfile)
164 } else {
165 Self::Child {
166 root: dotfile,
167 root_source_path: paths.root_source_path().to_path_buf(),
168 root_target_path: paths.root_target_path().to_path_buf(),
169 }
170 }
171 }
172
173 pub const fn dotfile(&self) -> &Dotfile {
175 match self {
176 Self::Root(dotfile) => dotfile,
177 Self::Child { root: dotfile, .. } => dotfile,
178 }
179 }
180}
181
182#[derive(Debug)]
184pub struct Item<'a> {
185 pub relative_source_path: PathBuf,
187
188 pub source_path: PathBuf,
190
191 pub target_path: PathBuf,
193
194 pub kind: Kind<'a>,
196}
197
198impl<'a> Item<'a> {
199 fn new(source: &PunktfSource, paths: Paths, dotfile: &'a Dotfile) -> Self {
201 let source_path = paths.child_source_path().into_owned();
202 let target_path = paths.child_target_path().into_owned();
203 let relative_source_path = source_path
204 .strip_prefix(&source.dotfiles)
205 .expect("Dotfile is not in the dotfile root")
206 .to_path_buf();
207 let kind = Kind::from_paths(paths, dotfile);
208
209 Self {
210 relative_source_path,
211 source_path,
212 target_path,
213 kind,
214 }
215 }
216}
217
218impl Item<'_> {
219 pub const fn dotfile(&self) -> &Dotfile {
221 self.kind.dotfile()
222 }
223}
224
225#[derive(Debug)]
227pub struct File<'a>(Item<'a>);
228
229impl<'a> Deref for File<'a> {
230 type Target = Item<'a>;
231
232 fn deref(&self) -> &Self::Target {
233 &self.0
234 }
235}
236
237#[derive(Debug)]
239pub struct Directory<'a>(Item<'a>);
240
241impl<'a> Deref for Directory<'a> {
242 type Target = Item<'a>;
243
244 fn deref(&self) -> &Self::Target {
245 &self.0
246 }
247}
248
249#[derive(Debug)]
251pub struct Symlink {
252 pub source_path: PathBuf,
254
255 pub target_path: PathBuf,
257
258 pub replace: bool,
261}
262
263#[derive(Debug)]
265pub struct Rejected<'a> {
266 pub item: Item<'a>,
268
269 pub reason: Cow<'static, str>,
271}
272
273impl<'a> Deref for Rejected<'a> {
274 type Target = Item<'a>;
275
276 fn deref(&self) -> &Self::Target {
277 &self.item
278 }
279}
280
281#[derive(Debug)]
283pub struct Errored<'a> {
284 pub item: Item<'a>,
286
287 pub error: Option<Box<dyn std::error::Error>>,
289
290 pub context: Option<Cow<'a, str>>,
292}
293
294impl<'a> Deref for Errored<'a> {
295 type Target = Item<'a>;
296
297 fn deref(&self) -> &Self::Target {
298 &self.item
299 }
300}
301
302impl fmt::Display for Errored<'_> {
303 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
304 let has_context = if let Some(context) = &self.context {
305 f.write_str(context)?;
306 true
307 } else {
308 false
309 };
310
311 if let Some(err) = &self.error {
312 if has_context {
313 f.write_str(": ")?;
314 }
315 write!(f, "{err}")?;
316 }
317
318 Ok(())
319 }
320}
321
322pub trait Visitor {
327 fn accept_file<'a>(
329 &mut self,
330 source: &PunktfSource,
331 profile: &LayeredProfile,
332 file: &File<'a>,
333 ) -> Result;
334
335 fn accept_directory<'a>(
337 &mut self,
338 source: &PunktfSource,
339 profile: &LayeredProfile,
340 directory: &Directory<'a>,
341 ) -> Result;
342
343 fn accept_link(
345 &mut self,
346 source: &PunktfSource,
347 profile: &LayeredProfile,
348 symlink: &Symlink,
349 ) -> Result;
350
351 fn accept_rejected<'a>(
357 &mut self,
358 source: &PunktfSource,
359 profile: &LayeredProfile,
360 rejected: &Rejected<'a>,
361 ) -> Result;
362
363 fn accept_errored<'a>(
369 &mut self,
370 source: &PunktfSource,
371 profile: &LayeredProfile,
372 errored: &Errored<'a>,
373 ) -> Result;
374}
375
376#[derive(Debug)]
379pub struct Walker<'a> {
380 profile: &'a LayeredProfile,
384}
385
386impl<'a> Walker<'a> {
387 pub fn new(profile: &'a mut LayeredProfile) -> Self {
393 {
394 let dotfiles = &mut profile.dotfiles;
395 dotfiles.sort_by_key(|(_, d)| -(d.priority.map(|p| p.0).unwrap_or(0) as i64));
397 };
398
399 Self { profile }
400 }
401
402 pub fn walk(&self, source: &PunktfSource, visitor: &mut impl Visitor) -> Result {
404 for dotfile in self.profile.dotfiles() {
405 self.walk_dotfile(source, visitor, dotfile)?;
406 }
407
408 for link in self.profile.symlinks() {
409 self.walk_link(source, visitor, link)?;
410 }
411
412 Ok(())
413 }
414
415 fn walk_dotfile(
417 &self,
418 source: &PunktfSource,
419 visitor: &mut impl Visitor,
420 dotfile: &Dotfile,
421 ) -> Result {
422 let source_path = match self.resolve_source_path(source, dotfile) {
423 Ok(p) => p,
424 Err(err) => {
425 let paths = Paths::new(dotfile.path.clone(), dotfile.path.clone());
426
427 return self.walk_errored(
428 source,
429 visitor,
430 paths,
431 dotfile,
432 Some(err),
433 Some("Failed to resolve source path of dotfile"),
434 );
435 }
436 };
437
438 let target_path = match self.resolve_target_path(dotfile, source_path.is_dir()) {
439 Ok(p) => p,
440 Err(err) => {
441 let paths = Paths::new(dotfile.path.clone(), dotfile.path.clone());
442
443 return self.walk_errored(
444 source,
445 visitor,
446 paths,
447 dotfile,
448 Some(err),
449 Some("Failed to resolve target path of dotfile"),
450 );
451 }
452 };
453
454 let paths = Paths::new(source_path, target_path);
455
456 if !paths.child_source_path().exists() {
457 let context = format!(
458 "Dotfile at {} does not exist",
459 paths.child_source_path().display()
460 );
461
462 return self.walk_errored(
463 source,
464 visitor,
465 paths,
466 dotfile,
467 None::<io::Error>,
468 Some(context),
469 );
470 };
471
472 self.walk_path(source, visitor, paths, dotfile)
473 }
474
475 fn walk_path(
479 &self,
480 source: &PunktfSource,
481 visitor: &mut impl Visitor,
482 paths: Paths,
483 dotfile: &Dotfile,
484 ) -> Result {
485 let source_path = paths.child_source_path();
486
487 if !self.accept(&source_path) {
488 return self.walk_rejected(source, visitor, paths, dotfile);
489 }
490
491 let metadata = match source_path.symlink_metadata() {
494 Ok(metadata) => metadata,
495 Err(err) => {
496 return self.walk_errored(
497 source,
498 visitor,
499 paths,
500 dotfile,
501 Some(err),
502 Some("Failed to resolve metadata"),
503 );
504 }
505 };
506
507 if metadata.is_file() {
508 self.walk_file(source, visitor, paths, dotfile)
509 } else if metadata.is_dir() {
510 self.walk_directory(source, visitor, paths, dotfile)
511 } else {
512 let err = io::Error::new(io::ErrorKind::Unsupported, "Invalid file type");
513
514 self.walk_errored(source, visitor, paths, dotfile, Some(err), None::<&str>)
515 }
516 }
517
518 fn walk_file(
520 &self,
521 source: &PunktfSource,
522 visitor: &mut impl Visitor,
523 paths: Paths,
524 dotfile: &Dotfile,
525 ) -> Result {
526 let file = File(Item::new(source, paths, dotfile));
527
528 visitor.accept_file(source, self.profile, &file)
529 }
530
531 fn walk_directory(
535 &self,
536 source: &PunktfSource,
537 visitor: &mut impl Visitor,
538 paths: Paths,
539 dotfile: &Dotfile,
540 ) -> Result {
541 let source_path = paths.child_source_path();
542
543 let directory = Directory(Item::new(source, paths.clone(), dotfile));
544
545 visitor.accept_directory(source, self.profile, &directory)?;
546
547 let read_dir = match std::fs::read_dir(source_path) {
548 Ok(path) => path,
549 Err(err) => {
550 return self.walk_errored(
551 source,
552 visitor,
553 paths,
554 dotfile,
555 Some(err),
556 Some("Failed to read directory"),
557 );
558 }
559 };
560
561 for dent in read_dir {
562 let dent = match dent {
563 Ok(dent) => dent,
564 Err(err) => {
565 return self.walk_errored(
566 source,
567 visitor,
568 paths,
569 dotfile,
570 Some(err),
571 Some("Failed to read directory"),
572 );
573 }
574 };
575
576 self.walk_path(
577 source,
578 visitor,
579 paths.clone().with_child(dent.file_name()),
580 dotfile,
581 )?;
582 }
583
584 Ok(())
585 }
586
587 fn walk_link(
589 &self,
590 source: &PunktfSource,
591 visitor: &mut impl Visitor,
592 link: &link::Symlink,
593 ) -> Result {
594 let link = Symlink {
597 source_path: self.resolve_path(&link.source_path)?,
598 target_path: self.resolve_path(&link.target_path)?,
599 replace: link.replace,
600 };
601
602 visitor.accept_link(source, self.profile, &link)
603 }
604
605 fn walk_rejected(
607 &self,
608 source: &PunktfSource,
609 visitor: &mut impl Visitor,
610 paths: Paths,
611 dotfile: &Dotfile,
612 ) -> Result {
613 let rejected = Rejected {
614 item: Item::new(source, paths, dotfile),
615 reason: Cow::Borrowed("Rejected by filter"),
616 };
617
618 visitor.accept_rejected(source, self.profile, &rejected)
619 }
620
621 fn walk_errored(
623 &self,
624 source: &PunktfSource,
625 visitor: &mut impl Visitor,
626 paths: Paths,
627 dotfile: &Dotfile,
628 error: Option<impl std::error::Error + 'static>,
629 context: Option<impl Into<Cow<'a, str>>>,
630 ) -> Result {
631 let errored = Errored {
632 item: Item::new(source, paths, dotfile),
633 error: error.map(|e| e.into()),
634 context: context.map(|c| c.into()),
635 };
636
637 visitor.accept_errored(source, self.profile, &errored)
638 }
639
640 fn resolve_path(&self, path: &Path) -> io::Result<PathBuf> {
643 let Some(path_str) = path.to_str() else {
644 return Err(io::Error::new(
645 io::ErrorKind::InvalidInput,
646 "File path includes non UTF-8 characters",
647 ));
648 };
649
650 shellexpand::full(path_str)
651 .map(|resolved| PathBuf::from(resolved.as_ref()))
652 .map_err(|err| io::Error::new(io::ErrorKind::Other, err))
653 }
654
655 fn resolve_source_path(&self, source: &PunktfSource, dotfile: &Dotfile) -> io::Result<PathBuf> {
657 self.resolve_path(&source.dotfiles.join(&dotfile.path))
658 }
659
660 fn resolve_target_path(&self, dotfile: &Dotfile, is_dir: bool) -> io::Result<PathBuf> {
664 let path = if is_dir && dotfile.rename.is_none() && dotfile.overwrite_target.is_none() {
665 self.profile
666 .target_path()
667 .expect("No target path set")
668 .to_path_buf()
669 } else {
670 dotfile
671 .overwrite_target
672 .as_deref()
673 .unwrap_or_else(|| self.profile.target_path().expect("No target path set"))
674 .join(dotfile.rename.as_ref().unwrap_or(&dotfile.path))
675 };
676
677 self.resolve_path(&path)
678 }
679
680 const fn accept(&self, _path: &Path) -> bool {
682 true
684 }
685}
686
687pub trait TemplateVisitor: Visitor {
690 fn accept_template<'a>(
695 &mut self,
696 source: &PunktfSource,
697 profile: &LayeredProfile,
698 file: &File<'a>,
699 resolve_content: impl FnOnce(&str) -> color_eyre::Result<String>,
702 ) -> Result;
703}
704
705#[derive(Debug)]
712pub struct ResolvingVisitor<V>(V);
713
714impl<V> ResolvingVisitor<V>
715where
716 V: TemplateVisitor,
717{
718 #[allow(clippy::missing_const_for_fn)]
720 pub fn into_inner(self) -> V {
721 self.0
722 }
723}
724
725impl<V: TemplateVisitor> Visitor for ResolvingVisitor<V> {
726 fn accept_file<'a>(
727 &mut self,
728 source: &PunktfSource,
729 profile: &LayeredProfile,
730 file: &File<'a>,
731 ) -> Result {
732 if file.dotfile().is_template() {
733 let resolve_fn = |content: &str| {
734 let source = Source::file(&file.source_path, content);
735 let template = Template::parse(source)
736 .with_context(|| format!("File: {}", file.source_path.display()))?;
737
738 template
739 .resolve(Some(profile.variables()), file.dotfile().variables.as_ref())
740 .with_context(|| format!("File: {}", file.source_path.display()))
741 };
742
743 self.0.accept_template(source, profile, file, resolve_fn)
744 } else {
745 self.0.accept_file(source, profile, file)
746 }
747 }
748
749 fn accept_directory<'a>(
750 &mut self,
751 source: &PunktfSource,
752 profile: &LayeredProfile,
753 directory: &Directory<'a>,
754 ) -> Result {
755 self.0.accept_directory(source, profile, directory)
756 }
757
758 fn accept_link(
759 &mut self,
760 source: &PunktfSource,
761 profile: &LayeredProfile,
762 symlink: &Symlink,
763 ) -> Result {
764 self.0.accept_link(source, profile, symlink)
765 }
766
767 fn accept_rejected<'a>(
768 &mut self,
769 source: &PunktfSource,
770 profile: &LayeredProfile,
771 rejected: &Rejected<'a>,
772 ) -> Result {
773 self.0.accept_rejected(source, profile, rejected)
774 }
775
776 fn accept_errored<'a>(
777 &mut self,
778 source: &PunktfSource,
779 profile: &LayeredProfile,
780 errored: &Errored<'a>,
781 ) -> Result {
782 self.0.accept_errored(source, profile, errored)
783 }
784}