Skip to main content

forc_crypto/
args.rs

1//! A `forc` plugin for converting a given string or path to their hash.
2
3use std::{
4    fs::read,
5    io::{self, BufRead},
6    path::Path,
7};
8
9forc_util::cli_examples! {
10    crate::Command {
11       [ Hashes an argument with SHA256 => "forc crypto sha256 test" ]
12       [ Hashes an argument with Keccak256 => "forc crypto keccak256 test" ]
13       [ Hashes a file path with SHA256 => "forc crypto sha256 {file}" ]
14       [ Hashes a file path with Keccak256 => "forc crypto keccak256 {file}" ]
15    }
16}
17
18#[derive(Debug, Clone, clap::Args)]
19#[clap(
20    version,
21    about = "Hashes the argument or file with this algorithm",
22    after_help = help(),
23)]
24pub struct HashArgs {
25    /// This argument is optional, it can be either:
26    ///
27    /// 1. A path to a file. If that is the case, the content of the file is
28    ///    loaded
29    ///
30    /// 2. A binary string encoded as a hex string. If that is the case, the
31    ///    hex is decoded and passed as a Vec<u8>
32    ///
33    /// 3. A string. This is the last option, if the string is "-", "stdin"
34    ///    is read instead. Otherwise the raw string is converted to a Vec<u8>
35    ///    and passed
36    ///
37    /// 4. If it is not provided, "stdin" is read
38    content_or_filepath: Option<String>,
39}
40
41fn checked_read_file<P: AsRef<Path>>(path: &Option<P>) -> Option<Vec<u8>> {
42    path.as_ref().map(read)?.ok()
43}
44
45fn checked_read_stdin<R: BufRead>(content: &Option<String>, mut stdin: R) -> Option<Vec<u8>> {
46    match content.as_ref().map(|x| x.as_str()) {
47        Some("-") | None => {
48            let mut buffer = Vec::new();
49            if stdin.read_to_end(&mut buffer).is_ok() {
50                Some(buffer)
51            } else {
52                Some(vec![])
53            }
54        }
55        _ => None,
56    }
57}
58
59fn read_as_binary(content: &Option<String>) -> Vec<u8> {
60    content
61        .as_ref()
62        .map(|x| {
63            if let Some(hex) = x.trim().strip_prefix("0x") {
64                if let Ok(bin) = hex::decode(hex) {
65                    bin
66                } else {
67                    x.as_bytes().to_vec()
68                }
69            } else {
70                x.as_bytes().to_vec()
71            }
72        })
73        .unwrap_or_default()
74}
75
76/// Reads the arg and returns a vector of bytes
77///
78/// These are the rules
79///  1. If None, stdin is read.
80///  2. If it's a String and it happens to be a file path, its content will be returned
81///  3. If it's a String and it is "-", stdin is read
82///  4. If the string starts with "0x", it will be treated as a hex string. Only
83///     fully valid hex strings are accepted.
84///  5. Otherwise the String will be converted to a vector of bytes
85pub fn read_content_filepath_or_stdin(arg: Option<String>) -> Vec<u8> {
86    match checked_read_file(&arg) {
87        Some(bytes) => bytes,
88        None => match checked_read_stdin(&arg, io::stdin().lock()) {
89            Some(bytes) => bytes,
90            None => read_as_binary(&arg),
91        },
92    }
93}
94
95/// The HashArgs takes no or a single argument, it can be either a string or a
96/// path to a file. It can be consumed and converted to a Vec<u8> using the From
97/// trait.
98///
99/// This is a wrapper around `read_content_filepath_or_stdin`
100impl From<HashArgs> for Vec<u8> {
101    fn from(value: HashArgs) -> Self {
102        read_content_filepath_or_stdin(value.content_or_filepath)
103    }
104}
105
106#[cfg(test)]
107mod test {
108    use super::*;
109
110    #[test]
111    fn test_checked_read_file() {
112        assert!(checked_read_file(&Some("not a file")).is_none());
113        assert!(checked_read_file(&Some("Cargo.toml")).is_some());
114        assert!(checked_read_file::<String>(&None).is_none());
115    }
116
117    #[test]
118    fn test_checked_stdin() {
119        let stdin = b"I'm a test from stdin";
120        assert_eq!(
121            None,
122            checked_read_stdin(&Some("value".to_owned()), &stdin[..])
123        );
124        assert_eq!(
125            Some(b"I'm a test from stdin".to_vec()),
126            checked_read_stdin(&None, &stdin[..])
127        );
128        assert_eq!(
129            Some(b"I'm a test from stdin".to_vec()),
130            checked_read_stdin(&Some("-".to_owned()), &stdin[..])
131        );
132        assert_eq!(None, checked_read_stdin(&Some("".to_owned()), &stdin[..]));
133    }
134
135    #[test]
136    fn test_read_binary() {
137        let x = "      0xff";
138        assert_eq!(vec![255u8], read_as_binary(&Some(x.to_owned())));
139        let x = "0xFF";
140        assert_eq!(vec![255u8], read_as_binary(&Some(x.to_owned())));
141        let x = " 0xFf";
142        assert_eq!(vec![255u8], read_as_binary(&Some(x.to_owned())));
143        let x = " 0xFfx";
144        assert_eq!(b" 0xFfx".to_vec(), read_as_binary(&Some(x.to_owned())));
145        let x = " some random data\n\n\0";
146        assert_eq!(
147            b" some random data\n\n\0".to_vec(),
148            read_as_binary(&Some(x.to_owned()))
149        );
150    }
151}