use std::hash::{DefaultHasher, Hash, Hasher};
pub const REC_VER_CONTAINER: u16 = 0xF;
#[allow(dead_code)]
pub const RT_DGG_CONTAINER: u16 = 0xF000; pub const RT_BSTORE_CONTAINER: u16 = 0xF001; pub const RT_FBSE: u16 = 0xF007; pub const RT_OPT: u16 = 0xF00B; pub const RT_TERTIARY_OPT: u16 = 0xF122;
pub const RT_BLIP_EMF: u16 = 0xF01A;
pub const RT_BLIP_WMF: u16 = 0xF01B;
pub const RT_BLIP_PICT: u16 = 0xF01C;
pub const RT_BLIP_JPEG: u16 = 0xF01D;
pub const RT_BLIP_PNG: u16 = 0xF01E;
pub const RT_BLIP_DIB: u16 = 0xF01F;
pub const RT_BLIP_TIFF: u16 = 0xF029;
pub const RT_BLIP_JPEG_CMYK: u16 = 0xF02A;
pub const PROP_PIB: u16 = 0x0104;
pub const FBSE_HEADER_LEN: usize = 36;
pub const FBSE_FO_DELAY_OFFSET: usize = 28;
#[derive(Debug, Clone, Copy)]
pub struct OdrawHeader {
pub rec_ver: u16,
pub rec_instance: u16,
pub rec_type: u16,
pub body_start: usize,
pub body_end: usize,
}
fn u16_le(data: &[u8], at: usize) -> Option<u16> {
data.get(at..at + 2)
.map(|b| u16::from_le_bytes([b[0], b[1]]))
}
fn u32_le(data: &[u8], at: usize) -> Option<u32> {
data.get(at..at + 4)
.map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
}
pub fn parse_odraw_header(data: &[u8], pos: usize, end: usize) -> Option<(OdrawHeader, usize)> {
if pos + 8 > end || end > data.len() {
return None;
}
let ver_inst = u16_le(data, pos)?;
let rec_type = u16_le(data, pos + 2)?;
let rec_len = u32_le(data, pos + 4)? as usize;
let body_start = pos + 8;
let body_end = body_start.saturating_add(rec_len).min(end);
Some((
OdrawHeader {
rec_ver: ver_inst & 0x000F,
rec_instance: ver_inst >> 4,
rec_type,
body_start,
body_end,
},
body_end,
))
}
pub fn is_blip_record(rec_type: u16) -> bool {
matches!(
rec_type,
RT_BLIP_EMF
| RT_BLIP_WMF
| RT_BLIP_PICT
| RT_BLIP_JPEG
| RT_BLIP_PNG
| RT_BLIP_DIB
| RT_BLIP_TIFF
| RT_BLIP_JPEG_CMYK
)
}
#[derive(Debug, Clone)]
pub struct DecodedBlip {
pub ext: &'static str,
pub bytes: Vec<u8>,
}
fn looks_like_jpeg(bytes: &[u8]) -> bool {
bytes.len() > 2 && bytes[0] == 0xFF && bytes[1] == 0xD8
}
fn looks_like_png(bytes: &[u8]) -> bool {
bytes.len() > 8 && bytes[..8] == [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]
}
pub fn decode_blip(rec_type: u16, rec_instance: u16, body: &[u8]) -> Option<DecodedBlip> {
let (ext, double_uid_instance, check): (&'static str, u16, fn(&[u8]) -> bool) = match rec_type {
RT_BLIP_JPEG | RT_BLIP_JPEG_CMYK => (".jpg", 0x46B, looks_like_jpeg),
RT_BLIP_PNG => (".png", 0x6E1, looks_like_png),
_ => return None,
};
let has_double_uid = rec_instance == double_uid_instance
|| (matches!(rec_type, RT_BLIP_JPEG | RT_BLIP_JPEG_CMYK) && rec_instance == 0x6E3);
let preferred = if has_double_uid { 33 } else { 17 };
let fallback = if has_double_uid { 17 } else { 33 };
for offset in [preferred, fallback] {
if let Some(data) = body.get(offset..) {
if check(data) {
return Some(DecodedBlip {
ext,
bytes: data.to_vec(),
});
}
}
}
None
}
pub fn decode_blip_record_at(data: &[u8], pos: usize) -> Option<DecodedBlip> {
let (hdr, _) = parse_odraw_header(data, pos, data.len())?;
if !is_blip_record(hdr.rec_type) {
return None;
}
decode_blip(
hdr.rec_type,
hdr.rec_instance,
data.get(hdr.body_start..hdr.body_end)?,
)
}
pub fn decode_fbse_blip(fbse_body: &[u8], delay_stream: Option<&[u8]>) -> Option<DecodedBlip> {
if fbse_body.len() > FBSE_HEADER_LEN {
let cb_name = fbse_body.get(33).copied().unwrap_or(0) as usize;
let blip_start = FBSE_HEADER_LEN + cb_name;
if blip_start < fbse_body.len() {
if let Some(decoded) = decode_blip_record_at(fbse_body, blip_start) {
return Some(decoded);
}
}
}
let delay = delay_stream?;
let fo_delay = u32_le(fbse_body, FBSE_FO_DELAY_OFFSET)? as usize;
if fo_delay == 0xFFFF_FFFF || fo_delay >= delay.len() {
return None;
}
decode_blip_record_at(delay, fo_delay)
}
pub fn find_record(data: &[u8], start: usize, end: usize, wanted_type: u16) -> Option<OdrawHeader> {
let mut pos = start;
while let Some((hdr, next)) = parse_odraw_header(data, pos, end) {
if next <= pos {
break;
}
if hdr.rec_type == wanted_type {
return Some(hdr);
}
if hdr.rec_ver == REC_VER_CONTAINER {
if let Some(found) = find_record(data, hdr.body_start, hdr.body_end, wanted_type) {
return Some(found);
}
}
pos = next;
}
None
}
pub fn collect_pib_values(data: &[u8], start: usize, end: usize, out: &mut Vec<u32>) {
let mut pos = start;
while let Some((hdr, next)) = parse_odraw_header(data, pos, end) {
if next <= pos {
break;
}
if hdr.rec_type == RT_OPT || hdr.rec_type == RT_TERTIARY_OPT {
let count = hdr.rec_instance as usize;
let body = &data[hdr.body_start..hdr.body_end];
for i in 0..count {
let off = i * 6;
let (Some(opid), Some(value)) = (u16_le(body, off), u32_le(body, off + 2)) else {
break;
};
if opid & 0x3FFF == PROP_PIB {
out.push(value);
}
}
} else if hdr.rec_ver == REC_VER_CONTAINER {
collect_pib_values(data, hdr.body_start, hdr.body_end, out);
}
pos = next;
}
}
pub fn blip_hash_name(bytes: &[u8], ext: &str) -> String {
let mut hasher = DefaultHasher::new();
bytes.hash(&mut hasher);
format!("{:016x}{ext}", hasher.finish())
}
#[cfg(test)]
mod tests {
use super::*;
const PNG_MAGIC: [u8; 8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
fn record(ver_inst: u16, rec_type: u16, body: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(8 + body.len());
out.extend_from_slice(&ver_inst.to_le_bytes());
out.extend_from_slice(&rec_type.to_le_bytes());
out.extend_from_slice(&(body.len() as u32).to_le_bytes());
out.extend_from_slice(body);
out
}
fn png_blip_body(double_uid: bool) -> Vec<u8> {
let uid_len = if double_uid { 32 } else { 16 };
let mut body = vec![0u8; uid_len + 1]; body.extend_from_slice(&PNG_MAGIC);
body.extend_from_slice(b"payload");
body
}
#[test]
fn decode_png_blip_single_uid() {
let body = png_blip_body(false);
let decoded = decode_blip(RT_BLIP_PNG, 0x6E0, &body).expect("png should decode");
assert_eq!(decoded.ext, ".png");
assert!(looks_like_png(&decoded.bytes));
}
#[test]
fn decode_png_blip_double_uid() {
let body = png_blip_body(true);
let decoded = decode_blip(RT_BLIP_PNG, 0x6E1, &body).expect("png should decode");
assert!(looks_like_png(&decoded.bytes));
}
#[test]
fn decode_blip_wrong_instance_falls_back_to_magic_scan() {
let body = png_blip_body(true);
let decoded = decode_blip(RT_BLIP_PNG, 0x6E0, &body).expect("magic fallback");
assert!(looks_like_png(&decoded.bytes));
}
#[test]
fn decode_jpeg_blip() {
let mut body = vec![0u8; 17];
body.extend_from_slice(&[0xFF, 0xD8, 0xFF, 0xE0, 1, 2, 3]);
let decoded = decode_blip(RT_BLIP_JPEG, 0x46A, &body).expect("jpeg should decode");
assert_eq!(decoded.ext, ".jpg");
assert_eq!(decoded.bytes[0], 0xFF);
}
#[test]
fn unsupported_blip_types_return_none() {
let body = vec![0u8; 64];
assert!(decode_blip(RT_BLIP_EMF, 0x3D4, &body).is_none());
assert!(decode_blip(RT_BLIP_WMF, 0x216, &body).is_none());
assert!(decode_blip(RT_BLIP_DIB, 0x7A8, &body).is_none());
}
#[test]
fn decode_blip_record_at_walks_header() {
let rec = record(0x6E0 << 4, RT_BLIP_PNG, &png_blip_body(false));
let decoded = decode_blip_record_at(&rec, 0).expect("record should decode");
assert_eq!(decoded.ext, ".png");
}
#[test]
fn decode_fbse_embedded_blip() {
let blip = record(0x6E0 << 4, RT_BLIP_PNG, &png_blip_body(false));
let mut fbse = vec![0u8; FBSE_HEADER_LEN];
fbse.extend_from_slice(&blip);
let decoded = decode_fbse_blip(&fbse, None).expect("embedded blip");
assert_eq!(decoded.ext, ".png");
}
#[test]
fn decode_fbse_delay_stream_blip() {
let blip = record(0x6E0 << 4, RT_BLIP_PNG, &png_blip_body(false));
let mut delay = vec![0u8; 10]; let fo_delay = delay.len() as u32;
delay.extend_from_slice(&blip);
let mut fbse = vec![0u8; FBSE_HEADER_LEN];
fbse[FBSE_FO_DELAY_OFFSET..FBSE_FO_DELAY_OFFSET + 4]
.copy_from_slice(&fo_delay.to_le_bytes());
let decoded = decode_fbse_blip(&fbse, Some(&delay)).expect("delay blip");
assert_eq!(decoded.ext, ".png");
}
#[test]
fn collect_pib_finds_property() {
let mut body = Vec::new();
body.extend_from_slice(&0x0181u16.to_le_bytes()); body.extend_from_slice(&0u32.to_le_bytes());
body.extend_from_slice(&0x4104u16.to_le_bytes()); body.extend_from_slice(&3u32.to_le_bytes());
let rec = record((2 << 4) | 0x3, RT_OPT, &body);
let mut pibs = Vec::new();
collect_pib_values(&rec, 0, rec.len(), &mut pibs);
assert_eq!(pibs, vec![3]);
}
#[test]
fn find_record_descends_containers() {
let inner = record(0, RT_FBSE, &[0u8; 4]);
let container = record((0 << 4) | REC_VER_CONTAINER, RT_BSTORE_CONTAINER, &inner);
let found = find_record(&container, 0, container.len(), RT_FBSE);
assert!(found.is_some());
}
#[test]
fn blip_hash_name_matches_scheme() {
let name = blip_hash_name(b"abc", ".png");
assert!(name.ends_with(".png"));
assert_eq!(name.len(), 16 + 4);
}
}