1pub mod age;
2pub mod gpg;
3
4use anyhow::anyhow;
5use std::fs::File;
6use std::io::{BufRead, BufReader, Read};
7use std::path::Path;
8use std::process::{ExitStatus, Output};
9
10#[derive(Clone, Copy, Debug)]
11pub enum Backend {
12 Age,
13 Gpg,
14}
15
16impl Backend {
17 pub fn encrypt(
18 &self,
19 mut content: impl Read,
20 store: &Path,
21 out_path: &Path,
22 ) -> anyhow::Result<()> {
23 match self {
24 Backend::Age => {
25 age::encrypt(&mut content, &self.recipient(store)?, out_path)?;
26 }
27 Backend::Gpg => {
28 gpg::encrypt(&mut content, &self.recipient(store)?, out_path)?;
29 }
30 }
31 Ok(())
32 }
33
34 pub fn decrypt(path: &Path) -> anyhow::Result<String> {
35 if is_age_encrypted(path)? {
36 age::decrypt(path)
37 } else {
38 gpg::decrypt(path)
39 }
40 }
41
42 pub fn display_name(&self) -> &'static str {
43 match self {
44 Backend::Age => "age",
45 Backend::Gpg => "gpg",
46 }
47 }
48
49 pub fn recipient_file_name(&self) -> &'static str {
50 match self {
51 Backend::Age => age::RECIPIENT_FILE_NAME,
52 Backend::Gpg => gpg::RECIPIENT_FILE_NAME,
53 }
54 }
55
56 pub fn needs_init(self, store: &Path) -> Option<Backend> {
57 if store.join(self.recipient_file_name()).exists() {
58 None
59 } else {
60 Some(self)
61 }
62 }
63
64 fn recipient(&self, dir: &Path) -> anyhow::Result<String> {
65 let recipient_file = dir.join(self.recipient_file_name());
66 read_first_line(&recipient_file)
67 }
68}
69
70fn is_age_encrypted(path: &Path) -> anyhow::Result<bool> {
71 let first_line = read_first_line(path)?;
72 Ok(
73 first_line.contains("BEGIN AGE ENCRYPTED FILE")
74 || first_line.contains("age-encryption.org"),
75 )
76}
77
78fn read_first_line(path: &Path) -> anyhow::Result<String> {
79 let file = File::open(path)?;
80 let first_line = BufReader::new(file)
81 .lines()
82 .next()
83 .ok_or(anyhow!("{path:?} is empty"))??;
84 Ok(first_line)
85}
86
87fn exit_status_to_result(exit_status: ExitStatus, bin_name: &str) -> anyhow::Result<()> {
88 if exit_status.success() {
89 Ok(())
90 } else {
91 Err(anyhow!(
92 "{bin_name} failed with exit code {}",
93 exit_status
94 .code()
95 .map(|c| c.to_string())
96 .unwrap_or_else(|| "<unknown>".to_string())
97 ))
98 }
99}
100
101fn output_to_result(mut output: Output) -> anyhow::Result<String> {
102 if output.status.success() {
103 while [Some(&b'\r'), Some(&b'\n')].contains(&output.stdout.last()) {
104 output.stdout.pop();
105 }
106 Ok(String::from_utf8(output.stdout)?)
107 } else {
108 Err(anyhow!(String::from_utf8(output.stderr)?))
109 }
110}