use crate::WpdError;
const WIRE_VERSION: u8 = 1;
const MIN_METADATA_ENTRY_BYTES: usize = 8;
const MIN_EVENT_BYTES: usize = 1;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WpdEvent {
Text(String),
Tab,
Space,
LineBreak,
ParagraphEnd,
ListItemStart {
ordered: bool,
level: u8,
counter: u32,
},
ListItemEnd,
HeadingStart {
level: u8,
},
BoldStart,
BoldEnd,
ItalicStart,
ItalicEnd,
UnderlineStart,
UnderlineEnd,
StrikethroughStart,
StrikethroughEnd,
SuperscriptStart,
SuperscriptEnd,
SubscriptStart,
SubscriptEnd,
TableStart,
RowStart {
header: bool,
},
CellStart {
column: i32,
col_span: u32,
row_span: u32,
},
CoveredCell {
column: i32,
},
CellEnd,
RowEnd,
TableEnd,
HeaderStart,
HeaderEnd,
FooterStart,
FooterEnd,
NoteStart {
endnote: bool,
},
NoteEnd,
AsideStart {
kind: String,
},
AsideEnd,
LinkStart {
href: String,
},
LinkEnd,
Field(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct WpdMetadata {
pub title: Option<String>,
pub author: Option<String>,
pub subject: Option<String>,
pub keywords: Option<String>,
pub raw: Vec<(String, String)>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct WpdDocument {
pub events: Vec<WpdEvent>,
pub metadata: WpdMetadata,
}
struct Reader<'a> {
bytes: &'a [u8],
pos: usize,
}
impl<'a> Reader<'a> {
fn new(bytes: &'a [u8]) -> Self {
Self { bytes, pos: 0 }
}
fn remaining(&self) -> usize {
self.bytes.len().saturating_sub(self.pos)
}
fn u8(&mut self) -> Result<u8, WpdError> {
let b = *self.bytes.get(self.pos).ok_or(WpdError::Internal)?;
self.pos += 1;
Ok(b)
}
fn bool(&mut self) -> Result<bool, WpdError> {
match self.u8()? {
0 => Ok(false),
1 => Ok(true),
_ => Err(WpdError::Internal),
}
}
fn u32(&mut self) -> Result<u32, WpdError> {
let end = self.pos.checked_add(4).ok_or(WpdError::Internal)?;
let slice = self.bytes.get(self.pos..end).ok_or(WpdError::Internal)?;
self.pos = end;
Ok(u32::from_le_bytes(slice.try_into().expect("slice is exactly 4 bytes")))
}
fn i32(&mut self) -> Result<i32, WpdError> {
self.u32().map(|v| v as i32)
}
fn string(&mut self) -> Result<String, WpdError> {
let len = self.u32()? as usize;
let end = self.pos.checked_add(len).ok_or(WpdError::Internal)?;
let slice = self.bytes.get(self.pos..end).ok_or(WpdError::Internal)?;
self.pos = end;
String::from_utf8(slice.to_vec()).map_err(|_| WpdError::InvalidUtf8)
}
}
pub fn decode(bytes: &[u8]) -> Result<WpdDocument, WpdError> {
let mut r = Reader::new(bytes);
let version = r.u8()?;
if version != WIRE_VERSION {
return Err(WpdError::Internal);
}
let metadata_count = r.u32()?;
let raw_cap = (metadata_count as usize).min(r.remaining() / MIN_METADATA_ENTRY_BYTES);
let mut raw = Vec::with_capacity(raw_cap);
let mut metadata = WpdMetadata::default();
for _ in 0..metadata_count {
let key = r.string()?;
let value = r.string()?;
match key.as_str() {
"dc:title" => metadata.title = Some(value.clone()),
"meta:initial-creator" => metadata.author = Some(value.clone()),
"dc:subject" => metadata.subject = Some(value.clone()),
"meta:keyword" => metadata.keywords = Some(value.clone()),
_ => {}
}
raw.push((key, value));
}
metadata.raw = raw;
let event_count = r.u32()?;
let events_cap = (event_count as usize).min(r.remaining() / MIN_EVENT_BYTES);
let mut events = Vec::with_capacity(events_cap);
for _ in 0..event_count {
events.push(decode_event(&mut r)?);
}
Ok(WpdDocument { events, metadata })
}
fn decode_event(r: &mut Reader<'_>) -> Result<WpdEvent, WpdError> {
let tag = r.u8()?;
Ok(match tag {
0 => WpdEvent::Text(r.string()?),
1 => WpdEvent::Tab,
2 => WpdEvent::Space,
3 => WpdEvent::LineBreak,
4 => WpdEvent::ParagraphEnd,
5 => {
let ordered = r.bool()?;
let level = r.u8()?;
let counter = r.u32()?;
WpdEvent::ListItemStart {
ordered,
level,
counter,
}
}
6 => WpdEvent::ListItemEnd,
7 => WpdEvent::HeadingStart { level: r.u8()? },
8 => WpdEvent::BoldStart,
9 => WpdEvent::BoldEnd,
10 => WpdEvent::ItalicStart,
11 => WpdEvent::ItalicEnd,
12 => WpdEvent::UnderlineStart,
13 => WpdEvent::UnderlineEnd,
14 => WpdEvent::StrikethroughStart,
15 => WpdEvent::StrikethroughEnd,
16 => WpdEvent::SuperscriptStart,
17 => WpdEvent::SuperscriptEnd,
18 => WpdEvent::SubscriptStart,
19 => WpdEvent::SubscriptEnd,
20 => WpdEvent::TableStart,
21 => WpdEvent::RowStart { header: r.bool()? },
22 => {
let column = r.i32()?;
let col_span = r.u32()?;
let row_span = r.u32()?;
WpdEvent::CellStart {
column,
col_span,
row_span,
}
}
23 => WpdEvent::CoveredCell { column: r.i32()? },
24 => WpdEvent::CellEnd,
25 => WpdEvent::RowEnd,
26 => WpdEvent::TableEnd,
27 => WpdEvent::HeaderStart,
28 => WpdEvent::HeaderEnd,
29 => WpdEvent::FooterStart,
30 => WpdEvent::FooterEnd,
31 => WpdEvent::NoteStart { endnote: r.bool()? },
32 => WpdEvent::NoteEnd,
33 => WpdEvent::AsideStart { kind: r.string()? },
34 => WpdEvent::AsideEnd,
35 => WpdEvent::LinkStart { href: r.string()? },
36 => WpdEvent::LinkEnd,
37 => WpdEvent::Field(r.string()?),
_ => return Err(WpdError::Internal),
})
}
#[cfg(test)]
mod tests {
use super::*;
fn string_bytes(s: &str) -> Vec<u8> {
let mut out = (s.len() as u32).to_le_bytes().to_vec();
out.extend_from_slice(s.as_bytes());
out
}
#[test]
fn decode_rejects_unknown_version() {
let bytes = vec![99u8, 0, 0, 0, 0, 0, 0, 0, 0];
assert!(matches!(decode(&bytes), Err(WpdError::Internal)));
}
#[test]
fn decode_rejects_truncated_input() {
assert!(matches!(decode(&[1]), Err(WpdError::Internal)));
assert!(matches!(decode(&[1, 0, 0]), Err(WpdError::Internal)));
}
#[test]
fn decode_rejects_unknown_event_tag() {
let mut bytes = vec![WIRE_VERSION];
bytes.extend_from_slice(&0u32.to_le_bytes()); bytes.extend_from_slice(&1u32.to_le_bytes()); bytes.push(255); assert!(matches!(decode(&bytes), Err(WpdError::Internal)));
}
#[test]
fn decode_rejects_abort_sized_count_without_allocating() {
assert!(matches!(
decode(&[WIRE_VERSION, 0xFF, 0xFF, 0xFF, 0xFF]),
Err(WpdError::Internal)
));
let mut bytes = vec![WIRE_VERSION];
bytes.extend_from_slice(&0u32.to_le_bytes()); bytes.extend_from_slice(&u32::MAX.to_le_bytes());
assert!(matches!(decode(&bytes), Err(WpdError::Internal)));
}
#[test]
fn decode_maps_initial_creator_to_author_not_typist() {
let mut bytes = vec![WIRE_VERSION];
bytes.extend_from_slice(&2u32.to_le_bytes());
bytes.extend(string_bytes("dc:creator"));
bytes.extend(string_bytes("The Typist"));
bytes.extend(string_bytes("meta:initial-creator"));
bytes.extend(string_bytes("The Author"));
bytes.extend_from_slice(&0u32.to_le_bytes());
let doc = decode(&bytes).expect("valid document");
assert_eq!(doc.metadata.author.as_deref(), Some("The Author"));
assert!(
doc.metadata
.raw
.iter()
.any(|(k, v)| k == "dc:creator" && v == "The Typist")
);
}
#[test]
fn decode_parses_metadata_and_events() {
let mut bytes = vec![WIRE_VERSION];
bytes.extend_from_slice(&2u32.to_le_bytes());
bytes.extend(string_bytes("dc:title"));
bytes.extend(string_bytes("Sample"));
bytes.extend(string_bytes("dc:type"));
bytes.extend(string_bytes("report"));
bytes.extend_from_slice(&3u32.to_le_bytes());
bytes.push(1); bytes.push(7); bytes.push(2); bytes.push(35); bytes.extend(string_bytes("https://example.com"));
let doc = decode(&bytes).expect("valid document");
assert_eq!(doc.metadata.title.as_deref(), Some("Sample"));
assert_eq!(
doc.metadata.raw,
vec![
("dc:title".to_string(), "Sample".to_string()),
("dc:type".to_string(), "report".to_string()),
]
);
assert_eq!(
doc.events,
vec![
WpdEvent::Tab,
WpdEvent::HeadingStart { level: 2 },
WpdEvent::LinkStart {
href: "https://example.com".to_string()
},
]
);
}
}