const LZFU_INIT_DICT: &[u8] = b"{\\rtf1\\ansi\\mac\\deff0\\deftab720{\\fonttbl;}{\\f0\\fnil \\froman \\fswiss \\fmodern \\fscript \\fdecor MS Sans SerifSymbolArialTimes New RomanCourier{\\colortbl\\red0\\green0\\blue0\r\n\\par \\pard\\plain\\f0\\fs20\\b\\i\\u\\tab\\tx";
const MAGIC_COMPRESSED: u32 = 0x7546_5A4C; const MAGIC_UNCOMPRESSED: u32 = 0x414C_454D; const DICT_SIZE: usize = 4096;
fn u32_le(data: &[u8], off: usize) -> u32 {
u32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
}
pub fn decompress_rtf(data: &[u8]) -> Result<Vec<u8>, String> {
if data.len() < 16 {
return Err("Compressed RTF stream too short".to_string());
}
let raw_size = u32_le(data, 4) as usize;
let magic = u32_le(data, 8);
let body = &data[16..];
if magic == MAGIC_UNCOMPRESSED {
let end = raw_size.min(body.len());
return Ok(body[..end].to_vec());
}
if magic != MAGIC_COMPRESSED {
return Err(format!("Unknown compressed-RTF magic 0x{magic:08X}"));
}
let mut dict = vec![0u8; DICT_SIZE];
dict[..LZFU_INIT_DICT.len()].copy_from_slice(LZFU_INIT_DICT);
let mut write_pos = LZFU_INIT_DICT.len(); let mut out = Vec::with_capacity(raw_size);
let mut ip = 0usize;
'outer: while ip < body.len() {
let flags = body[ip];
ip += 1;
for bit in 0..8 {
if (flags >> bit) & 1 == 0 {
if ip >= body.len() {
break 'outer;
}
let b = body[ip];
ip += 1;
out.push(b);
dict[write_pos % DICT_SIZE] = b;
write_pos = (write_pos + 1) % DICT_SIZE;
} else {
if ip + 1 >= body.len() {
break 'outer;
}
let b1 = body[ip] as usize;
let b2 = body[ip + 1] as usize;
ip += 2;
let offset = (b1 << 4) | (b2 >> 4);
let length = (b2 & 0x0F) + 2;
if offset == write_pos {
break 'outer; }
for i in 0..length {
let b = dict[(offset + i) % DICT_SIZE];
out.push(b);
dict[write_pos % DICT_SIZE] = b;
write_pos = (write_pos + 1) % DICT_SIZE;
}
}
if out.len() >= raw_size {
break 'outer;
}
}
}
out.truncate(raw_size);
Ok(out)
}
pub fn rtf_to_text(rtf_bytes: &[u8]) -> String {
let rtf = String::from_utf8_lossy(rtf_bytes);
match rtf_parser::RtfDocument::try_from(rtf.as_ref()) {
Ok(doc) => doc.get_text(),
Err(_) => String::new(),
}
}
fn normalize_whitespace(text: &str) -> String {
text.split("\n\n")
.map(|para| para.split_whitespace().collect::<Vec<_>>().join(" "))
.filter(|p| !p.is_empty())
.collect::<Vec<_>>()
.join("\n\n")
}
pub fn compressed_rtf_to_text(compressed: &[u8]) -> Option<String> {
let raw = decompress_rtf(compressed).ok()?;
let text = normalize_whitespace(&rtf_to_text(&raw));
let trimmed = text.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn init_dict_is_207_bytes() {
assert_eq!(LZFU_INIT_DICT.len(), 207);
}
}