happy_cracking/crypto/
strings.rs1use 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(¤t).into_owned());
88 }
89 current.clear();
90 }
91 }
92 if current.len() >= min_len {
93 results.push(String::from_utf8_lossy(¤t).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(¤t).into_owned());
117 }
118 current.clear();
119 i += 1;
120 }
121 }
122 if current.len() >= min_len {
123 results.push(String::from_utf8_lossy(¤t).into_owned());
124 }
125
126 Ok(results)
127}