fencryption_lib/commands/
logic.rs

1//! Logic used in commands.
2
3use std::{
4    fs::{self, OpenOptions},
5    io::{Read, Write},
6    path::{Path, PathBuf},
7};
8
9use serde::{Deserialize, Serialize};
10
11use crate::{
12    commands::{Command, ErrorBuilder, Result},
13    crypto::{self, Crypto},
14    metadata,
15};
16
17pub enum OutputDecPath {
18    Direct(PathBuf),
19    Parent(PathBuf),
20}
21
22#[derive(Serialize, Deserialize, Debug)]
23pub struct PathMetadata(PathBuf);
24
25impl PathMetadata {
26    pub fn new<P>(path: P) -> Self
27    where
28        P: AsRef<Path>,
29    {
30        PathMetadata(path.as_ref().to_owned())
31    }
32
33    pub fn path(&self) -> PathBuf {
34        self.0.to_owned()
35    }
36}
37
38pub fn checks<P>(input_paths: P, output_path: &Option<PathBuf>) -> Result<()>
39where
40    P: AsRef<Vec<PathBuf>>,
41{
42    if input_paths.as_ref().len() == 0 {
43        return Err(ErrorBuilder::new()
44            .message("Please provide at least one path")
45            .build());
46    }
47
48    if input_paths.as_ref().iter().any(|p| !p.exists()) {
49        return Err(ErrorBuilder::new()
50            .message("I can't work with files that don't exist")
51            .build());
52    }
53
54    if output_path.as_ref().is_some() && input_paths.as_ref().len() != 1 {
55        return Err(ErrorBuilder::new()
56            .message("Only one input path can be provided when setting an output path")
57            .build());
58    }
59
60    Ok(())
61}
62
63pub fn change_file_name<P, F>(path: P, callback: F) -> PathBuf
64where
65    P: AsRef<Path>,
66    F: FnOnce(&str) -> String,
67{
68    let mut path = path.as_ref().to_owned();
69    path.set_file_name(
70        [callback(
71            path.file_name()
72                .unwrap_or_default()
73                .to_str()
74                .unwrap_or_default(),
75        )]
76        .concat(),
77    );
78    path
79}
80
81pub fn get_output_paths(
82    paths: &Vec<PathBuf>,
83    output_path: &Option<PathBuf>,
84    command: Command,
85) -> Vec<PathBuf> {
86    paths
87        .iter()
88        .map(|p| match output_path {
89            Some(p) => p.to_owned(),
90            None => change_file_name(p, |s| match command {
91                Command::EncryptFile => [s, ".enc"].concat(),
92                Command::DecryptFile => {
93                    if s.ends_with(".enc") {
94                        s.replace(".enc", ".dec")
95                    } else {
96                        [s, ".dec"].concat()
97                    }
98                } // _ => panic!(),
99            }),
100        })
101        .collect::<Vec<PathBuf>>()
102}
103
104pub fn overwrite<P>(paths: P, overwrite: bool) -> Result<()>
105where
106    P: AsRef<[PathBuf]>,
107{
108    if paths.as_ref().iter().any(|p| p.exists()) {
109        println!("{:#?}, {}", paths.as_ref()[0], paths.as_ref()[0].exists());
110        if overwrite {
111            for path in paths.as_ref() {
112                delete_entry(&path).map_err(|e| {
113                    ErrorBuilder::new()
114                        .message("Failed to overwrite file/directory, please do it yourself")
115                        .error(e)
116                        .build()
117                })?
118            }
119        } else {
120            return Err(ErrorBuilder::new()
121                .message("The output file/directory already exists (use \"--overwrite\"/\"-O\" to force overwrite)")
122                .build());
123        }
124    };
125
126    Ok(())
127}
128
129pub fn delete_original<P>(path: P, delete_original: bool) -> Result<()>
130where
131    P: AsRef<Path>,
132{
133    if delete_original && path.as_ref().exists() {
134        delete_entry(path.as_ref()).map_err(|e| {
135            ErrorBuilder::new()
136                .message("Failed to delete original file/directory, please do it yourself")
137                .error(e)
138                .build()
139        })?;
140    }
141
142    Ok(())
143}
144
145pub fn delete_entry<P>(path: P) -> Result<()>
146where
147    P: AsRef<Path>,
148{
149    if path.as_ref().is_dir() {
150        fs::remove_dir_all(path.as_ref()).map_err(|e| {
151            ErrorBuilder::new()
152                .message("Failed to remove directory, please do it yourself")
153                .error(e)
154                .build()
155        })?;
156    } else if path.as_ref().is_file() {
157        fs::remove_file(path.as_ref()).map_err(|e| {
158            ErrorBuilder::new()
159                .message("Failed to remove file, please do it yourself")
160                .error(e)
161                .build()
162        })?;
163    }
164
165    Ok(())
166}
167
168/// Encrypts a file.
169///
170/// If `with_metadata` is true, writes metadata at the start
171/// of the file.
172pub fn encrypt_file<P1, P2>(
173    crypto: Crypto,
174    input_path: P1,
175    output_path: P2,
176    relative_path: Option<PathBuf>,
177) -> Result<()>
178where
179    P1: AsRef<Path>,
180    P2: AsRef<Path>,
181{
182    let mut source = OpenOptions::new()
183        .read(true)
184        .open(&input_path)
185        .map_err(|e| {
186            ErrorBuilder::new()
187                .message("Failed to read source file")
188                .error(e)
189                .build()
190        })?;
191
192    let mut dest = OpenOptions::new()
193        .write(true)
194        .create(true)
195        .open(&output_path)
196        .map_err(|e| {
197            ErrorBuilder::new()
198                .message("Failed to open/create destination file")
199                .error(e)
200                .build()
201        })?;
202
203    if let Some(p) = relative_path {
204        let metadata = metadata::encode(PathMetadata::new(p)).map_err(|e| {
205            ErrorBuilder::new()
206                .message("Failed to encode file metadata")
207                .error(e)
208                .build()
209        })?;
210
211        let encrypted_metadata = crypto.encrypt(metadata).map_err(|e| {
212            ErrorBuilder::new()
213                .message("Failed to encrypt metadata")
214                .error(e)
215                .build()
216        })?;
217
218        dest.write_all(
219            &[
220                (encrypted_metadata.len() as u16).to_be_bytes().as_ref(),
221                encrypted_metadata.as_ref(),
222            ]
223            .concat(),
224        )
225        .map_err(|e| {
226            ErrorBuilder::new()
227                .message("Failed to write metadata")
228                .error(e)
229                .build()
230        })?;
231    };
232
233    crypto.encrypt_io(&mut source, &mut dest).map_err(|e| {
234        ErrorBuilder::new()
235            .message(match e {
236                crypto::ErrorKind::AesError(_) => "Failed to encrypt file (key must be wrong)",
237                _ => "Failed to encrypt file",
238            })
239            .error(e)
240            .build()
241    })?;
242
243    Ok(())
244}
245
246/// Decrypts a file.
247///
248/// If output_path is None, the function will try to extract
249/// metadata from the file.
250pub fn decrypt_file<P>(crypto: Crypto, input_path: P, output_path: OutputDecPath) -> Result<()>
251where
252    P: AsRef<Path>,
253{
254    let mut source = OpenOptions::new()
255        .read(true)
256        .open(&input_path)
257        .map_err(|e| {
258            ErrorBuilder::new()
259                .message("Failed to read source file")
260                .error(e)
261                .build()
262        })?;
263
264    let output_path = match output_path {
265        OutputDecPath::Direct(p) => p,
266        OutputDecPath::Parent(p) => {
267            let mut len_bytes = [0u8; 2];
268            source.read_exact(&mut len_bytes).map_err(|e| {
269                ErrorBuilder::new()
270                    .message("Failed to get encrypted metadata length")
271                    .error(e)
272                    .build()
273            })?;
274            let len = u16::from_be_bytes(len_bytes) as usize;
275            let mut metadata_bytes = vec![0u8; len];
276            source.read_exact(&mut metadata_bytes).map_err(|e| {
277                ErrorBuilder::new()
278                    .message("Failed to get encrypted metadata")
279                    .error(e)
280                    .build()
281            })?;
282            let metadata = metadata::decode::<PathMetadata>(
283                &crypto.decrypt(&metadata_bytes).map_err(|e| {
284                    ErrorBuilder::new()
285                        .message("Failed to decrypt metadata")
286                        .error(e)
287                        .build()
288                })?,
289            )
290            .map_err(|e| {
291                ErrorBuilder::new()
292                    .message("Failed to decode metadata")
293                    .error(e)
294                    .build()
295            })?;
296
297            let path = p.join(metadata.path());
298            if let Some(p) = path.parent() {
299                fs::create_dir_all(p).map_err(|e| {
300                    ErrorBuilder::new()
301                        .message("Failed to create sub-directory")
302                        .error(e)
303                        .build()
304                })?
305            };
306            path
307        }
308    };
309
310    let mut dest = OpenOptions::new()
311        .write(true)
312        .create(true)
313        .open(&output_path)
314        .map_err(|e| {
315            ErrorBuilder::new()
316                .message("Failed to open/create destination file")
317                .error(e)
318                .build()
319        })?;
320
321    crypto.decrypt_io(&mut source, &mut dest).map_err(|e| {
322        ErrorBuilder::new()
323            .message(match e {
324                crypto::ErrorKind::AesError(_) => "Failed to decrypt file (key must be wrong)",
325                _ => "Failed to decrypt file",
326            })
327            .error(e)
328            .build()
329    })?;
330
331    Ok(())
332}