xlsbye-biff12 0.1.0

BIFF12 binary record parser for XLSB files
Documentation
use xlsbye_core::error::Result;
use xlsbye_core::types::ParsedChartsheet;

use crate::record::header::RecordIter;
use crate::strings::{decode_short_string, decode_short_string_u16, decode_wide_string};

pub fn parse_chartsheet(data: &[u8]) -> Result<ParsedChartsheet> {
    let mut drawing_rel_id = None;

    for record in RecordIter::new(data) {
        let (_record_type, payload) = record?;
        if drawing_rel_id.is_none() {
            drawing_rel_id = extract_rel_id(payload);
        }
    }

    Ok(ParsedChartsheet { drawing_rel_id })
}

fn extract_rel_id(payload: &[u8]) -> Option<String> {
    for offset in 0..payload.len() {
        if let Some((value, _)) = decode_wide_string(&payload[offset..]) {
            if is_rid(&value) {
                return Some(value);
            }
        }
        if let Some((value, _)) = decode_short_string(&payload[offset..]) {
            if is_rid(&value) {
                return Some(value);
            }
        }
        if let Some((value, _)) = decode_short_string_u16(&payload[offset..]) {
            if is_rid(&value) {
                return Some(value);
            }
        }
    }

    None
}

fn is_rid(value: &str) -> bool {
    let Some(suffix) = value.strip_prefix("rId") else {
        return false;
    };
    !suffix.is_empty() && suffix.chars().all(|ch| ch.is_ascii_digit())
}

#[cfg(test)]
mod tests {
    use super::*;

    fn encode_varint(mut value: u32) -> Vec<u8> {
        let mut out = Vec::new();
        loop {
            let mut byte = (value & 0x7F) as u8;
            value >>= 7;
            if value != 0 {
                byte |= 0x80;
            }
            out.push(byte);
            if value == 0 {
                break;
            }
        }
        out
    }

    fn encode_record(record_type: u16, payload: &[u8]) -> Vec<u8> {
        let mut out = Vec::new();
        out.extend_from_slice(&encode_varint(u32::from(record_type)));
        out.extend_from_slice(&encode_varint(payload.len() as u32));
        out.extend_from_slice(payload);
        out
    }

    fn encode_wide_string(value: &str) -> Vec<u8> {
        let utf16 = value.encode_utf16().collect::<Vec<_>>();
        let mut out = Vec::new();
        out.extend_from_slice(&(utf16.len() as u32).to_le_bytes());
        for unit in utf16 {
            out.extend_from_slice(&unit.to_le_bytes());
        }
        out
    }

    #[test]
    fn parses_chartsheet_drawing_relationship_id() {
        let mut payload = Vec::new();
        payload.extend_from_slice(&0u32.to_le_bytes());
        payload.extend_from_slice(&encode_wide_string("rId1"));

        let mut data = Vec::new();
        data.extend_from_slice(&encode_record(0x1234, &payload));

        let parsed = parse_chartsheet(&data).expect("chartsheet should parse");
        assert_eq!(parsed.drawing_rel_id.as_deref(), Some("rId1"));
    }
}