forc_crypto/
args.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
//! A `forc` plugin for converting a given string or path to their hash.

use std::{
    fs::read,
    io::{self, BufRead},
    path::Path,
};

forc_util::cli_examples! {
    crate::Command {
       [ Hashes an argument with SHA256 => "forc crypto sha256 test" ]
       [ Hashes an argument with Keccak256 => "forc crypto keccak256 test" ]
       [ Hashes a file path with SHA256 => "forc crypto sha256 {file}" ]
       [ Hashes a file path with Keccak256 => "forc crypto keccak256 {file}" ]
    }
}

#[derive(Debug, Clone, clap::Args)]
#[clap(
    version,
    about = "Hashes the argument or file with this algorithm",
    after_help = help(),
)]
pub struct HashArgs {
    /// This argument is optional, it can be either:
    ///
    /// 1. A path to a file. If that is the case, the content of the file is
    ///    loaded
    ///
    /// 2. A binary string encoded as a hex string. If that is the case, the
    ///    hex is decoded and passed as a Vec<u8>
    ///
    /// 3. A string. This is the last option, if the string is "-", "stdin"
    ///    is read instead. Otherwise the raw string is converted to a Vec<u8>
    ///    and passed
    ///
    /// 4. If it is not provided, "stdin" is read
    content_or_filepath: Option<String>,
}

fn checked_read_file<P: AsRef<Path>>(path: &Option<P>) -> Option<Vec<u8>> {
    path.as_ref().map(read)?.ok()
}

fn checked_read_stdin<R: BufRead>(content: &Option<String>, mut stdin: R) -> Option<Vec<u8>> {
    match content.as_ref().map(|x| x.as_str()) {
        Some("-") | None => {
            let mut buffer = Vec::new();
            if stdin.read_to_end(&mut buffer).is_ok() {
                Some(buffer)
            } else {
                Some(vec![])
            }
        }
        _ => None,
    }
}

fn read_as_binary(content: &Option<String>) -> Vec<u8> {
    content
        .as_ref()
        .map(|x| {
            if let Some(hex) = x.trim().strip_prefix("0x") {
                if let Ok(bin) = hex::decode(hex) {
                    bin
                } else {
                    x.as_bytes().to_vec()
                }
            } else {
                x.as_bytes().to_vec()
            }
        })
        .unwrap_or_default()
}

/// Reads the arg and returns a vector of bytes
///
/// These are the rules
///  1. If None, stdin is read.
///  2. If it's a String and it happens to be a file path, its content will be returned
///  3. If it's a String and it is "-", stdin is read
///  4. If the string starts with "0x", it will be treated as a hex string. Only
///     fully valid hex strings are accepted.
///  5. Otherwise the String will be converted to a vector of bytes
pub fn read_content_filepath_or_stdin(arg: Option<String>) -> Vec<u8> {
    match checked_read_file(&arg) {
        Some(bytes) => bytes,
        None => match checked_read_stdin(&arg, io::stdin().lock()) {
            Some(bytes) => bytes,
            None => read_as_binary(&arg),
        },
    }
}

/// The HashArgs takes no or a single argument, it can be either a string or a
/// path to a file. It can be consumed and converted to a Vec<u8> using the From
/// trait.
///
/// This is a wrapper around `read_content_filepath_or_stdin`
impl From<HashArgs> for Vec<u8> {
    fn from(value: HashArgs) -> Self {
        read_content_filepath_or_stdin(value.content_or_filepath)
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_checked_read_file() {
        assert!(checked_read_file(&Some("not a file")).is_none());
        assert!(checked_read_file(&Some("Cargo.toml")).is_some());
        assert!(checked_read_file::<String>(&None).is_none());
    }

    #[test]
    fn test_checked_stdin() {
        let stdin = b"I'm a test from stdin";
        assert_eq!(
            None,
            checked_read_stdin(&Some("value".to_owned()), &stdin[..])
        );
        assert_eq!(
            Some(b"I'm a test from stdin".to_vec()),
            checked_read_stdin(&None, &stdin[..])
        );
        assert_eq!(
            Some(b"I'm a test from stdin".to_vec()),
            checked_read_stdin(&Some("-".to_owned()), &stdin[..])
        );
        assert_eq!(None, checked_read_stdin(&Some("".to_owned()), &stdin[..]));
    }

    #[test]
    fn test_read_binary() {
        let x = "      0xff";
        assert_eq!(vec![255u8], read_as_binary(&Some(x.to_owned())));
        let x = "0xFF";
        assert_eq!(vec![255u8], read_as_binary(&Some(x.to_owned())));
        let x = " 0xFf";
        assert_eq!(vec![255u8], read_as_binary(&Some(x.to_owned())));
        let x = " 0xFfx";
        assert_eq!(b" 0xFfx".to_vec(), read_as_binary(&Some(x.to_owned())));
        let x = " some random data\n\n\0";
        assert_eq!(
            b" some random data\n\n\0".to_vec(),
            read_as_binary(&Some(x.to_owned()))
        );
    }
}