Skip to main content

happy_cracking/crypto/
strings.rs

1use anyhow::{Context, Result};
2use clap::{Subcommand, ValueEnum};
3use std::path::PathBuf;
4
5#[derive(Clone, Copy, ValueEnum)]
6pub enum StringEncoding {
7    Ascii,
8    Utf16le,
9    Both,
10}
11
12#[derive(Subcommand)]
13pub enum StringsAction {
14    #[command(about = "Extract printable strings from binary data")]
15    Extract {
16        #[arg(help = "Input as hex string (or use --file)")]
17        input: Option<String>,
18        #[arg(long, help = "Read input from a file path")]
19        file: Option<PathBuf>,
20        #[arg(long, default_value = "4", help = "Minimum string length")]
21        min_len: usize,
22        #[arg(long, default_value = "ascii", help = "Encoding to scan for")]
23        encoding: StringEncoding,
24    },
25}
26
27pub fn run(action: StringsAction) -> Result<()> {
28    match action {
29        StringsAction::Extract {
30            input,
31            file,
32            min_len,
33            encoding,
34        } => {
35            let data = match (input, file) {
36                (Some(_), Some(_)) => {
37                    anyhow::bail!("Provide exactly one of <input> or --file, not both")
38                }
39                (None, None) => {
40                    anyhow::bail!("Provide exactly one of <input> or --file")
41                }
42                (Some(hex_str), None) => {
43                    hex::decode(hex_str.trim()).context("Failed to decode input as hex")?
44                }
45                (None, Some(path)) => std::fs::read(&path)
46                    .with_context(|| format!("Failed to read file: {}", path.display()))?,
47            };
48
49            match encoding {
50                StringEncoding::Ascii => {
51                    for s in extract_ascii(&data, min_len)? {
52                        println!("{}", s);
53                    }
54                }
55                StringEncoding::Utf16le => {
56                    for s in extract_utf16le(&data, min_len)? {
57                        println!("{}", s);
58                    }
59                }
60                StringEncoding::Both => {
61                    for s in extract_ascii(&data, min_len)? {
62                        println!("{}", s);
63                    }
64                    for s in extract_utf16le(&data, min_len)? {
65                        println!("{}", s);
66                    }
67                }
68            }
69        }
70    }
71    Ok(())
72}
73
74pub fn extract_ascii(data: &[u8], min_len: usize) -> Result<Vec<String>> {
75    if min_len == 0 {
76        anyhow::bail!("min_len must be at least 1");
77    }
78
79    let mut results = Vec::new();
80    let mut current = Vec::new();
81
82    for &b in data {
83        if (0x20..=0x7E).contains(&b) {
84            current.push(b);
85        } else if !current.is_empty() {
86            if current.len() >= min_len {
87                results.push(String::from_utf8_lossy(&current).into_owned());
88            }
89            current.clear();
90        }
91    }
92    if current.len() >= min_len {
93        results.push(String::from_utf8_lossy(&current).into_owned());
94    }
95
96    Ok(results)
97}
98
99pub fn extract_utf16le(data: &[u8], min_len: usize) -> Result<Vec<String>> {
100    if min_len == 0 {
101        anyhow::bail!("min_len must be at least 1");
102    }
103
104    let mut results = Vec::new();
105    let mut current = Vec::new();
106    let mut i = 0;
107
108    while i + 1 < data.len() {
109        let lo = data[i];
110        let hi = data[i + 1];
111        if (0x20..=0x7E).contains(&lo) && hi == 0x00 {
112            current.push(lo);
113            i += 2;
114        } else {
115            if current.len() >= min_len {
116                results.push(String::from_utf8_lossy(&current).into_owned());
117            }
118            current.clear();
119            i += 1;
120        }
121    }
122    if current.len() >= min_len {
123        results.push(String::from_utf8_lossy(&current).into_owned());
124    }
125
126    Ok(results)
127}