pub fn extract_ascii_strings(data: &[u8], min_len: usize) -> Vec<String> {
let mut out = Vec::new();
let mut current = String::new();
for &b in data {
if (0x20..=0x7E).contains(&b) {
current.push(b as char);
} else if !current.is_empty() && current.len() >= min_len {
out.push(std::mem::take(&mut current));
} else {
current.clear();
}
}
if !current.is_empty() && current.len() >= min_len {
out.push(current);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extracts_simple_runs() {
let got = extract_ascii_strings(b"hello\0world\0", 3);
assert_eq!(got, vec!["hello", "world"]);
}
#[test]
fn applies_minimum_length() {
let got = extract_ascii_strings(b"hi\0ok\0longer\0", 5);
assert_eq!(got, vec!["longer"]);
}
#[test]
fn treats_tab_and_newline_as_separators() {
let got = extract_ascii_strings(b"alpha\tbeta\ngamma", 3);
assert_eq!(got, vec!["alpha", "beta", "gamma"]);
}
#[test]
fn handles_trailing_run_without_terminator() {
let got = extract_ascii_strings(b"prefix\0tail", 3);
assert_eq!(got, vec!["prefix", "tail"]);
}
#[test]
fn rejects_high_bit_bytes() {
let mut buf = b"good".to_vec();
buf.push(0xC3);
buf.push(0xA9);
buf.extend_from_slice(b"more");
let got = extract_ascii_strings(&buf, 3);
assert_eq!(got, vec!["good", "more"]);
}
#[test]
fn empty_input_returns_empty() {
let got = extract_ascii_strings(&[], 1);
assert!(got.is_empty());
}
#[test]
fn min_len_zero_emits_singletons() {
let got = extract_ascii_strings(b"a\0b", 0);
assert_eq!(got, vec!["a", "b"]);
}
#[test]
fn min_len_zero_skips_empty_runs_on_consecutive_separators() {
let got = extract_ascii_strings(b"\0\0abc", 0);
assert_eq!(got, vec!["abc"]);
let got = extract_ascii_strings(b"ab\0\0\0cd\0\0", 0);
assert_eq!(got, vec!["ab", "cd"]);
}
#[test]
fn del_character_0x7f_terminates_run() {
let mut buf = b"hello".to_vec();
buf.push(0x7F);
buf.extend_from_slice(b"world");
let got = extract_ascii_strings(&buf, 3);
assert_eq!(got, vec!["hello", "world"]);
}
#[test]
fn unit_separator_0x1f_terminates_run() {
let mut buf = b"abc".to_vec();
buf.push(0x1F);
buf.extend_from_slice(b"defg");
let got = extract_ascii_strings(&buf, 3);
assert_eq!(got, vec!["abc", "defg"]);
}
#[test]
fn space_0x20_included_in_run() {
let got = extract_ascii_strings(b"hello world\0", 5);
assert_eq!(got, vec!["hello world"]);
}
#[test]
fn tilde_0x7e_included_in_run() {
let got = extract_ascii_strings(b"hello~world\0", 3);
assert_eq!(got, vec!["hello~world"]);
}
#[test]
fn min_len_4_boundary() {
let got = extract_ascii_strings(b"abc\0abcd\0abcde\0", 4);
assert_eq!(got, vec!["abcd", "abcde"]);
}
#[test]
fn output_strings_are_pure_ascii() {
let mut buf = Vec::new();
for b in 0x20u8..=0x7Eu8 {
buf.push(b);
}
buf.push(0u8);
let got = extract_ascii_strings(&buf, 1);
assert_eq!(got.len(), 1);
for s in &got {
assert!(s.is_ascii(), "output must be pure ASCII: {:?}", s);
}
}
#[test]
fn large_buffer_trailing_run_emitted() {
let buf: Vec<u8> = (0..1000u32).map(|i| (0x41u8 + (i % 26) as u8)).collect();
let got = extract_ascii_strings(&buf, 1);
assert!(!got.is_empty());
let total: usize = got.iter().map(|s| s.len()).sum();
assert_eq!(total, 1000);
}
#[test]
fn no_empty_strings_in_output() {
let got = extract_ascii_strings(b"\x00\x00\x00hello\x00\x00\x00world\x00\x00", 3);
for s in &got {
assert!(!s.is_empty(), "output must contain no empty strings");
}
assert_eq!(got, vec!["hello", "world"]);
}
#[test]
fn exact_min_len_token_emitted() {
let got = extract_ascii_strings(b"\x00eng_\x00", 4);
assert_eq!(got, vec!["eng_"]);
}
#[test]
fn single_byte_at_min_len_1() {
let got = extract_ascii_strings(b"A\x00B\x00C", 1);
assert_eq!(got, vec!["A", "B", "C"]);
}
}