extend-docx 0.1.0

Rust parser, document model, and serializer for DOCX and legacy Word documents
Documentation
use super::{fib::fcidx, DocFile};

pub(super) struct Comment {
    pub reference_cp: u32,
    pub text_start: u32,
    pub text_end: u32,
    pub author: String,
    pub initials: String,
}

fn u16_at(bytes: &[u8], offset: usize) -> Option<u16> {
    Some(u16::from_le_bytes(
        bytes.get(offset..offset + 2)?.try_into().ok()?,
    ))
}

fn u32_at(bytes: &[u8], offset: usize) -> Option<u32> {
    Some(u32::from_le_bytes(
        bytes.get(offset..offset + 4)?.try_into().ok()?,
    ))
}

fn xst(bytes: &[u8], offset: usize) -> Option<(String, usize)> {
    let count = u16_at(bytes, offset)? as usize;
    let end = offset.checked_add(2 + count * 2)?;
    let units = bytes
        .get(offset + 2..end)?
        .chunks_exact(2)
        .map(|b| u16::from_le_bytes([b[0], b[1]]))
        .collect::<Vec<_>>();
    Some((String::from_utf16_lossy(&units), end))
}

pub(super) fn parse(doc: &DocFile) -> Vec<Comment> {
    let Some(refs) = doc.fib.table_slice(&doc.table, fcidx::PLCFAND_REF) else {
        return Vec::new();
    };
    let Some(text) = doc.fib.table_slice(&doc.table, fcidx::PLCFAND_TXT) else {
        return Vec::new();
    };
    let Some(payload) = refs.len().checked_sub(4).filter(|n| n % 34 == 0) else {
        return Vec::new();
    };
    let count = payload / 34;
    let owners = doc
        .fib
        .table_slice(&doc.table, fcidx::GRP_XST_ATN_OWNERS)
        .unwrap_or_default();
    let mut authors = Vec::new();
    let mut offset = 0;
    while let Some((author, end)) = xst(owners, offset) {
        authors.push(author);
        offset = end;
    }
    let base = doc
        .fib
        .ccp
        .hdd_start()
        .saturating_add(doc.fib.ccp.hdd)
        .saturating_add(doc.fib.ccp.mcr);
    (0..count)
        .filter_map(|index| {
            let reference_cp = u32_at(refs, index * 4)?;
            let start = u32_at(text, index * 4)?;
            let end = u32_at(text, (index + 1) * 4)?;
            if reference_cp >= doc.fib.ccp.text || start > end || end > doc.fib.ccp.atn {
                return None;
            }
            let atrd = (count + 1) * 4 + index * 30;
            let initials = xst(refs.get(atrd..atrd + 20)?, 0)
                .map(|v| v.0)
                .unwrap_or_default();
            let author = authors
                .get(u16_at(refs, atrd + 20)? as usize)
                .cloned()
                .unwrap_or_default();
            Some(Comment {
                reference_cp,
                text_start: base + start,
                text_end: base + end,
                author,
                initials,
            })
        })
        .collect()
}