use crate::extractors::rtf::encoding::parse_rtf_control_word;
pub struct RtfImage {
pub format: &'static str,
pub data: Vec<u8>,
}
pub(crate) fn extract_pict_image(chars: &mut std::iter::Peekable<std::str::Chars>) -> (String, Option<RtfImage>) {
let mut metadata = String::new();
let mut image_type: Option<&str> = None;
let mut format: &str = "jpeg";
let mut depth = 0;
let mut hex_chars = String::new();
let mut _has_bin = false;
while let Some(&ch) = chars.peek() {
match ch {
'{' => {
depth += 1;
chars.next();
}
'}' => {
if depth == 0 {
break;
}
depth -= 1;
chars.next();
}
'\\' => {
chars.next();
let (control_word, value) = parse_rtf_control_word(chars);
match control_word.as_str() {
"jpegblip" => {
image_type = Some("jpg");
format = "jpeg";
}
"pngblip" => {
image_type = Some("png");
format = "png";
}
"wmetafile" => {
image_type = Some("wmf");
format = "wmf";
}
"dibitmap" => {
image_type = Some("bmp");
format = "bmp";
}
"picwgoal" | "pichgoal" => {}
"bin" => {
if let Some(count) = value {
let count = count.max(0) as usize;
let mut consumed_bytes = 0usize;
while consumed_bytes < count {
match chars.next() {
Some(consumed_char) => {
consumed_bytes += if consumed_char == char::REPLACEMENT_CHARACTER {
1
} else {
consumed_char.len_utf8()
};
}
None => break,
}
}
_has_bin = true;
}
}
_ => {}
}
}
' ' | '\r' | '\n' => {
chars.next();
}
_ => {
if ch.is_ascii_hexdigit() {
hex_chars.push(ch);
}
chars.next();
}
}
}
if let Some(itype) = image_type {
metadata.push_str("image.");
metadata.push_str(itype);
}
if metadata.is_empty() {
metadata.push_str("image.jpg");
}
let image = if !hex_chars.is_empty() {
match hex::decode(&hex_chars) {
Ok(data) if !data.is_empty() => Some(RtfImage { format, data }),
_ => None,
}
} else {
None
};
(metadata, image)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_skip_bin_payload_by_byte_count_not_char_count() {
let input = "\\jpegblip\\bin4\u{00e9}AB}AFTER";
let mut chars = input.chars().peekable();
let (metadata, image) = extract_pict_image(&mut chars);
assert_eq!(metadata, "image.jpg");
assert!(image.is_none());
let remaining: String = chars.collect();
assert_eq!(
remaining, "}AFTER",
"the group's closing brace and the trailing document text must survive `\\bin` skipping"
);
}
#[test]
fn should_charge_replacement_characters_one_source_byte_each() {
let replacement = char::REPLACEMENT_CHARACTER;
let payload = format!("{replacement}{replacement}}}{replacement}{replacement}{replacement}");
let input = format!("\\jpegblip\\bin6{payload}}}AFTER");
let mut chars = input.chars().peekable();
let (metadata, image) = extract_pict_image(&mut chars);
assert_eq!(metadata, "image.jpg");
assert!(image.is_none());
let remaining: String = chars.collect();
assert_eq!(
remaining, "}AFTER",
"the six-byte payload — including its embedded `}}` byte — must be consumed whole, \
leaving only the group's real closing brace and the document text after it"
);
}
}