rpgmv_tool/command/
decrypt.rs

1use anyhow::anyhow;
2use anyhow::bail;
3use anyhow::ensure;
4use anyhow::Context;
5use glob::glob;
6use std::fs::File;
7use std::io::BufReader;
8use std::io::Write;
9use std::path::Path;
10use std::path::PathBuf;
11
12#[derive(Debug, argh::FromArgs)]
13#[argh(subcommand, name = "decrypt", description = "decrypt a file")]
14pub struct Options {
15    #[argh(option, long = "input", short = 'i', description = "a file to decrypt")]
16    pub input: Vec<PathBuf>,
17
18    #[argh(
19        option,
20        long = "glob-input",
21        description = "a glob of input files to decrypt"
22    )]
23    pub glob_input: Vec<String>,
24
25    #[argh(
26        option,
27        long = "output",
28        short = 'o',
29        description = "the output folder"
30    )]
31    pub output: PathBuf,
32}
33
34/// Try to get metadata for a path
35fn try_metadata<P>(path: P) -> std::io::Result<Option<std::fs::Metadata>>
36where
37    P: AsRef<Path>,
38{
39    match std::fs::metadata(path) {
40        Ok(metadata) => Ok(Some(metadata)),
41        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
42        Err(error) => Err(error),
43    }
44}
45
46/// Interface inspired by mv.
47/// See: https://man7.org/linux/man-pages/man1/mv.1p.html
48pub fn exec(options: Options) -> anyhow::Result<()> {
49    let mut inputs = options.input;
50    for input in options.glob_input {
51        let iter = glob(&input)?;
52        for input in iter {
53            let input = input?;
54
55            inputs.push(input);
56        }
57    }
58
59    ensure!(!inputs.is_empty(), "need at least 1 input");
60
61    let output_metadata = try_metadata(&options.output)
62        .with_context(|| format!("failed to stat \"{}\"", options.output.display()))?;
63
64    // If the output is a directory, use the vector impl.
65    match output_metadata {
66        Some(metadata) if metadata.is_dir() => {
67            return exec_vector(&inputs, &options.output);
68        }
69        Some(_) | None => {}
70    }
71
72    if inputs.len() == 1 {
73        let input = &inputs[0];
74
75        // For file destinations or non-existent destinations
76        // We filter out directory outputs earlier.
77        exec_scalar(input, &options.output)
78    } else {
79        // We can't use the scalar impl since there must be more than 1 input.
80        // We can't use the vector impl since the target output is either a file or does not exist.
81        // Assume the user wanted to use the vector impl for error, since there is more than 1 input.
82        Err(anyhow!(
83            "\"{}\" is not a directory or does not exist",
84            options.output.display()
85        ))
86    }
87}
88
89fn exec_scalar(input: &Path, output: &Path) -> anyhow::Result<()> {
90    decrypt_single_file(input, output)
91}
92
93fn exec_vector(inputs: &[PathBuf], output: &Path) -> anyhow::Result<()> {
94    for input in inputs.iter() {
95        let input_file_name = input
96            .file_name()
97            .with_context(|| format!("failed to get file name from \"{}\"", &input.display()))?;
98
99        let output = {
100            let mut path = output.join(input_file_name);
101            path.set_extension("png");
102            path
103        };
104
105        decrypt_single_file(input, &output)?;
106    }
107
108    Ok(())
109}
110
111fn decrypt_single_file(input: &Path, output: &Path) -> anyhow::Result<()> {
112    let output_metadata =
113        try_metadata(output).with_context(|| format!("failed to stat \"{}\"", output.display()))?;
114
115    if output_metadata.is_some() {
116        bail!(
117            "output path \"{}\" exists, refusing to overwrite",
118            output.display()
119        );
120    }
121
122    let file =
123        File::open(input).with_context(|| format!("failed to open \"{}\"", input.display()))?;
124
125    let file = BufReader::new(file);
126    let mut reader = rpgmvp::Reader::new(file);
127    reader.read_header().context("invalid header")?;
128    let key = reader.extract_key().context("failed to extract key")?;
129    let key_hex = base16ct::lower::encode_string(&key);
130    println!("Key for \"{}\": {}", input.display(), key_hex);
131
132    let output_tmp = nd_util::with_push_extension(output, "tmp");
133    let mut writer = File::create(&output_tmp)
134        .with_context(|| format!("failed to open \"{}\"", output_tmp.display()))?;
135    std::io::copy(&mut reader, &mut writer)?;
136    writer.flush()?;
137    writer.sync_all()?;
138    std::fs::rename(&output_tmp, output)?;
139
140    Ok(())
141}