xlsbye-biff12 0.1.0

BIFF12 binary record parser for XLSB files
Documentation
use xlsbye_core::error::{Result, XlsByeError};

use crate::record::header::RecordIter;
use crate::record::ids::{BRT_BEGIN_PIVOT_CACHE_DEF, BRT_END_PIVOT_CACHE_DEF};

#[derive(Debug, Clone, PartialEq)]
pub struct ParsedPivotTable {
    pub has_pivot_definition: bool,
    pub record_count: usize,
    pub raw_payload: Vec<u8>,
}

pub fn parse_pivot_table(data: &[u8]) -> Result<ParsedPivotTable> {
    let mut has_begin = false;
    let mut has_end = false;
    let mut record_count = 0usize;

    for record in RecordIter::new(data) {
        let (record_type, _) = record?;
        record_count = record_count.saturating_add(1);
        if record_type == BRT_BEGIN_PIVOT_CACHE_DEF.as_u16() {
            has_begin = true;
        }
        if record_type == BRT_END_PIVOT_CACHE_DEF.as_u16() {
            has_end = true;
        }
    }

    if has_begin && !has_end {
        return Err(XlsByeError::Biff12(
            "pivot table stream missing BrtEndPivotCacheDef".to_string(),
        ));
    }

    Ok(ParsedPivotTable {
        has_pivot_definition: has_begin,
        record_count,
        raw_payload: data.to_vec(),
    })
}

#[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
    }

    #[test]
    fn detects_pivot_table_stream_and_preserves_bytes() {
        let mut data = Vec::new();
        data.extend_from_slice(&encode_record(BRT_BEGIN_PIVOT_CACHE_DEF.as_u16(), &[1, 2]));
        data.extend_from_slice(&encode_record(0x0802, &[]));
        data.extend_from_slice(&encode_record(BRT_END_PIVOT_CACHE_DEF.as_u16(), &[]));

        let parsed = parse_pivot_table(&data).expect("pivot parse should succeed");
        assert!(parsed.has_pivot_definition);
        assert_eq!(parsed.record_count, 3);
        assert_eq!(parsed.raw_payload, data);
    }

    #[test]
    fn rejects_unterminated_pivot_definition() {
        let data = encode_record(BRT_BEGIN_PIVOT_CACHE_DEF.as_u16(), &[]);
        let err = parse_pivot_table(&data).expect_err("missing end should fail");
        assert!(format!("{err}").contains("BrtEndPivotCacheDef"));
    }
}