use serde::Serialize;
use crate::gobin::{GoBinary, SectionKind};
#[derive(Debug, Clone, Serialize)]
pub struct GoString {
pub addr: u64,
pub section: String,
pub text: String,
}
#[derive(Debug, Clone)]
pub struct Options {
pub min_len: usize,
pub max: Option<usize>,
pub filter: Option<String>,
}
impl Default for Options {
fn default() -> Self {
Options {
min_len: 8,
max: None,
filter: None,
}
}
}
pub fn extract(bin: &GoBinary, opts: &Options) -> Vec<GoString> {
let mut out = Vec::new();
for section in &bin.sections {
if !matches!(
section.kind,
SectionKind::ReadOnlyData | SectionKind::NoPtrData
) {
continue;
}
let end = section
.file_offset
.saturating_add(section.file_size)
.min(bin.bytes.len());
let slice = &bin.bytes[section.file_offset..end];
extract_runs(slice, section.addr, §ion.name, opts, &mut out);
if let Some(cap) = opts.max {
if out.len() >= cap {
out.truncate(cap);
return out;
}
}
}
out
}
fn extract_runs(
slice: &[u8],
section_addr: u64,
section_name: &str,
opts: &Options,
out: &mut Vec<GoString>,
) {
let mut i = 0usize;
while i < slice.len() {
let b = slice[i];
if !is_printable(b) {
i += 1;
continue;
}
let start = i;
while i < slice.len() && is_printable(slice[i]) {
i += 1;
}
let len = i - start;
if len < opts.min_len {
continue;
}
let bytes = &slice[start..i];
let text = std::str::from_utf8(bytes).expect("ASCII printable is UTF-8");
if let Some(needle) = &opts.filter {
if !text.contains(needle.as_str()) {
continue;
}
}
out.push(GoString {
addr: section_addr + (start as u64),
section: section_name.to_string(),
text: text.to_string(),
});
if let Some(cap) = opts.max {
if out.len() >= cap {
return;
}
}
}
}
#[inline]
fn is_printable(b: u8) -> bool {
(0x20..=0x7E).contains(&b)
}
#[cfg(test)]
mod tests {
use super::*;
fn opts(min_len: usize) -> Options {
Options {
min_len,
..Options::default()
}
}
#[test]
fn extracts_runs_above_min_len() {
let mut out = Vec::new();
let data = b"hello\x00world\x00ab\x00longerstring\x00";
extract_runs(data, 0x1000, ".rodata", &opts(5), &mut out);
let texts: Vec<&str> = out.iter().map(|s| s.text.as_str()).collect();
assert_eq!(texts, vec!["hello", "world", "longerstring"]);
assert_eq!(out[0].addr, 0x1000);
assert_eq!(out[1].addr, 0x1006);
assert_eq!(out[2].addr, 0x100f);
}
#[test]
fn drops_runs_below_min_len() {
let mut out = Vec::new();
extract_runs(b"abc\x00abcdefgh", 0, ".rodata", &opts(8), &mut out);
assert_eq!(out.len(), 1);
assert_eq!(out[0].text, "abcdefgh");
}
#[test]
fn nonprintable_terminates_run() {
let data = b"abcdefgh\x01ijklmnop";
let mut out = Vec::new();
extract_runs(data, 0, ".rodata", &opts(4), &mut out);
assert_eq!(out.len(), 2);
assert_eq!(out[0].text, "abcdefgh");
assert_eq!(out[1].text, "ijklmnop");
}
#[test]
fn newline_terminates_run() {
let data = b"line one is here\nline two follows here";
let mut out = Vec::new();
extract_runs(data, 0, ".rodata", &opts(4), &mut out);
assert_eq!(out.len(), 2);
assert_eq!(out[0].text, "line one is here");
assert_eq!(out[1].text, "line two follows here");
}
#[test]
fn substring_filter_drops_non_matches() {
let mut o = opts(4);
o.filter = Some("usage".into());
let data = b"random text here\x00usage: foo\x00more random\x00";
let mut out = Vec::new();
extract_runs(data, 0, ".rodata", &o, &mut out);
assert_eq!(out.len(), 1);
assert_eq!(out[0].text, "usage: foo");
}
#[test]
fn max_cap_truncates_output() {
let mut o = opts(4);
o.max = Some(2);
let data = b"first\x00second\x00third\x00fourth\x00";
let mut out = Vec::new();
extract_runs(data, 0, ".rodata", &o, &mut out);
assert_eq!(out.len(), 2);
assert_eq!(out[0].text, "first");
assert_eq!(out[1].text, "second");
}
}