rust-docx 0.2.16

Rust parser, document model, and serializer for DOCX and legacy Word documents
Documentation
use super::{
    comments::xst,
    fmt::{dttm_to_iso, escape_xml},
    sprm,
};

pub(super) fn authors(table: &[u8]) -> Vec<String> {
    if table.len() < 6 || table[..2] != [0xff, 0xff] {
        return Vec::new();
    }
    let count = u16::from_le_bytes([table[2], table[3]]);
    let extra = u16::from_le_bytes([table[4], table[5]]) as usize;
    let mut pos = 6;
    let mut names = Vec::new();
    for _ in 0..count {
        if table.get(pos..pos + 2) == Some(&[0xff, 0xff]) {
            names.push(String::new());
            pos += 2 + extra;
            continue;
        }
        let Some((name, end)) = xst(table, pos) else {
            break;
        };
        names.push(name);
        pos = end + extra;
    }
    names
}

#[derive(Default)]
pub(super) struct Mark {
    author: Option<usize>,
    date: Option<u32>,
}

impl Mark {
    pub fn open(&self, tag: &str, id: u32, authors: &[String]) -> String {
        let mut xml = format!(r#"<w:{tag} w:id="{id}""#);
        if let Some(author) = self
            .author
            .and_then(|i| authors.get(i))
            .filter(|s| !s.is_empty())
        {
            xml.push_str(&format!(r#" w:author="{}""#, escape_xml(author)));
        }
        if let Some(date) = self.date.and_then(dttm_to_iso) {
            xml.push_str(&format!(r#" w:date="{date}""#));
        }
        xml.push('>');
        xml
    }
}

pub(super) fn marks(properties: &[u8], deleted: bool) -> (Option<Mark>, Option<Mark>) {
    if !deleted && memchr::memmem::find(properties, &[0x01, 0x08]).is_none() {
        return (None, None);
    }
    let mut inserted = false;
    let (mut ins, mut del) = (Mark::default(), Mark::default());
    for p in sprm::iterate(properties) {
        match p.opcode {
            0x0801 => match p.byte() {
                0 => inserted = false,
                1 => inserted = true,
                0x81 => inserted = !inserted,
                _ => {}
            },
            0x4804 => ins.author = usize::try_from(p.i16_operand()).ok(),
            0x6805 => ins.date = Some(p.u32_operand()),
            0x4863 => del.author = usize::try_from(p.i16_operand()).ok(),
            0x6864 => del.date = Some(p.u32_operand()),
            _ => {}
        }
    }
    (inserted.then_some(ins), deleted.then_some(del))
}

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

    #[test]
    fn decodes_persisted_revision_metadata_without_inventing_a_zone() {
        let names = authors(&[255, 255, 2, 0, 0, 0, 0, 0, 1, 0, 65, 0]);
        let date = ((2025u32 - 1900) << 20) | (12 << 16) | (31 << 11) | (16 << 6) | 35;
        let mut chpx = vec![0x01, 0x08, 1, 0x04, 0x48, 1, 0, 0x05, 0x68];
        chpx.extend_from_slice(&date.to_le_bytes());
        let (insertion, deletion) = marks(&chpx, false);
        assert!(deletion.is_none());
        assert_eq!(
            insertion.unwrap().open("ins", 8, &names),
            r#"<w:ins w:id="8" w:author="A" w:date="2025-12-31T16:35:00">"#
        );
        assert!(dttm_to_iso(0).is_none());
    }
}