1use quick_xml::events::{BytesEnd, BytesStart, Event};
4use quick_xml::{Reader, Writer, XmlVersion};
5
6use crate::error::{OxmlError, Result};
7use crate::namespace::{W_NS, matches_local_name};
8use crate::shared::{ST_Border, ST_TabJc, ST_TabLeader};
9use crate::units::Twips;
10
11const MAX_TAB_XML_DEPTH: usize = 64;
12
13#[derive(Debug, Clone, PartialEq)]
15pub struct CT_BorderEdge {
16 pub val: ST_Border,
18 pub sz: Option<u32>,
20 pub space: Option<u32>,
22 pub color: Option<String>,
24}
25
26impl CT_BorderEdge {
27 pub fn new(val: ST_Border) -> Self {
28 CT_BorderEdge {
29 val,
30 sz: None,
31 space: None,
32 color: None,
33 }
34 }
35
36 pub fn from_xml_attrs(e: &BytesStart) -> Result<Self> {
37 let mut val = ST_Border::None;
38 let mut sz = None;
39 let mut space = None;
40 let mut color = None;
41
42 for attr in e.attributes() {
43 let attr = attr?;
44 let key = attr.key.as_ref();
45 let v = std::str::from_utf8(&attr.value)?;
46 if matches_local_name(key, b"val") {
47 val = ST_Border::from_str(v)?;
48 } else if matches_local_name(key, b"sz") {
49 sz = Some(v.parse()?);
50 } else if matches_local_name(key, b"space") {
51 space = Some(v.parse()?);
52 } else if matches_local_name(key, b"color") {
53 color = Some(v.to_string());
54 }
55 }
56
57 Ok(CT_BorderEdge {
58 val,
59 sz,
60 space,
61 color,
62 })
63 }
64
65 pub fn write_xml_attrs(&self, e: &mut BytesStart) {
66 let mut buf = itoa::Buffer::new();
67 e.push_attribute(("w:val", self.val.to_str()));
68 if let Some(sz) = self.sz {
69 e.push_attribute(("w:sz", buf.format(sz)));
70 }
71 if let Some(space) = self.space {
72 e.push_attribute(("w:space", buf.format(space)));
73 }
74 if let Some(ref color) = self.color {
75 e.push_attribute(("w:color", color.as_str()));
76 }
77 }
78
79 pub fn to_xml<W: std::io::Write>(
81 &self,
82 writer: &mut Writer<W>,
83 tag: &str,
84 ) -> crate::error::Result<()> {
85 let mut e = BytesStart::new(tag);
86 self.write_xml_attrs(&mut e);
87 writer.write_event(Event::Empty(e))?;
88 Ok(())
89 }
90}
91
92#[derive(Debug, Clone, Default, PartialEq)]
94pub struct CT_PBdr {
95 pub top: Option<CT_BorderEdge>,
96 pub bottom: Option<CT_BorderEdge>,
97 pub left: Option<CT_BorderEdge>,
98 pub right: Option<CT_BorderEdge>,
99 pub between: Option<CT_BorderEdge>,
100 pub bar: Option<CT_BorderEdge>,
101}
102
103impl CT_PBdr {
104 pub fn from_xml(reader: &mut Reader<&[u8]>) -> Result<Self> {
105 let mut bdr = CT_PBdr::default();
106 let mut buf = Vec::new();
107
108 loop {
109 match reader.read_event_into(&mut buf) {
110 Ok(Event::Empty(ref e)) => {
111 let name = e.name();
112 let edge = CT_BorderEdge::from_xml_attrs(e)?;
113 if matches_local_name(name.as_ref(), b"top") {
114 bdr.top = Some(edge);
115 } else if matches_local_name(name.as_ref(), b"bottom") {
116 bdr.bottom = Some(edge);
117 } else if matches_local_name(name.as_ref(), b"left")
118 || matches_local_name(name.as_ref(), b"start")
119 {
120 bdr.left = Some(edge);
121 } else if matches_local_name(name.as_ref(), b"right")
122 || matches_local_name(name.as_ref(), b"end")
123 {
124 bdr.right = Some(edge);
125 } else if matches_local_name(name.as_ref(), b"between") {
126 bdr.between = Some(edge);
127 } else if matches_local_name(name.as_ref(), b"bar") {
128 bdr.bar = Some(edge);
129 }
130 }
131 Ok(Event::End(ref e)) if matches_local_name(e.name().as_ref(), b"pBdr") => {
132 break;
133 }
134 Ok(Event::Eof) => break,
135 Err(e) => return Err(e.into()),
136 _ => {}
137 }
138 buf.clear();
139 }
140
141 Ok(bdr)
142 }
143
144 pub fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
145 writer.write_event(Event::Start(BytesStart::new("w:pBdr")))?;
146
147 if let Some(ref edge) = self.top {
148 let mut e = BytesStart::new("w:top");
149 edge.write_xml_attrs(&mut e);
150 writer.write_event(Event::Empty(e))?;
151 }
152 if let Some(ref edge) = self.left {
153 let mut e = BytesStart::new("w:left");
154 edge.write_xml_attrs(&mut e);
155 writer.write_event(Event::Empty(e))?;
156 }
157 if let Some(ref edge) = self.bottom {
158 let mut e = BytesStart::new("w:bottom");
159 edge.write_xml_attrs(&mut e);
160 writer.write_event(Event::Empty(e))?;
161 }
162 if let Some(ref edge) = self.right {
163 let mut e = BytesStart::new("w:right");
164 edge.write_xml_attrs(&mut e);
165 writer.write_event(Event::Empty(e))?;
166 }
167 if let Some(ref edge) = self.between {
168 let mut e = BytesStart::new("w:between");
169 edge.write_xml_attrs(&mut e);
170 writer.write_event(Event::Empty(e))?;
171 }
172 if let Some(ref edge) = self.bar {
173 let mut e = BytesStart::new("w:bar");
174 edge.write_xml_attrs(&mut e);
175 writer.write_event(Event::Empty(e))?;
176 }
177
178 writer.write_event(Event::End(BytesEnd::new("w:pBdr")))?;
179 Ok(())
180 }
181
182 pub fn is_empty(&self) -> bool {
183 self.top.is_none()
184 && self.bottom.is_none()
185 && self.left.is_none()
186 && self.right.is_none()
187 && self.between.is_none()
188 && self.bar.is_none()
189 }
190}
191
192#[derive(Debug, Clone)]
194pub struct CT_TabStop {
195 pub val: ST_TabJc,
197 pub pos: Twips,
199 pub leader: Option<ST_TabLeader>,
201 pub source_occurrence: Option<usize>,
206}
207
208impl PartialEq for CT_TabStop {
209 fn eq(&self, other: &Self) -> bool {
210 self.val == other.val && self.pos == other.pos && self.leader == other.leader
211 }
212}
213
214impl CT_TabStop {
215 pub fn new(val: ST_TabJc, pos: Twips) -> Self {
216 CT_TabStop {
217 val,
218 pos,
219 leader: None,
220 source_occurrence: None,
221 }
222 }
223
224 pub fn from_xml_attrs(e: &BytesStart) -> Result<Self> {
225 Self::from_xml_attrs_with_prefixes(e, &["w".to_string()])
226 }
227
228 pub fn from_xml_attrs_with_prefixes(e: &BytesStart, word_prefixes: &[String]) -> Result<Self> {
230 let prefixes = word_prefixes_at(e, word_prefixes)?;
231 let mut val = ST_TabJc::Left;
232 let mut pos = Twips(0);
233 let mut leader = None;
234
235 for attr in e.attributes() {
236 let attr = attr?;
237 let key = attr.key.as_ref();
238 let v = std::str::from_utf8(&attr.value)?;
239 if is_word_attribute(key, b"val", &prefixes) {
240 val = ST_TabJc::from_str(v)?;
241 } else if is_word_attribute(key, b"pos", &prefixes) {
242 pos = Twips(v.parse()?);
243 } else if is_word_attribute(key, b"leader", &prefixes) {
244 leader = Some(ST_TabLeader::from_str(v)?);
245 }
246 }
247
248 Ok(CT_TabStop {
249 val,
250 pos,
251 leader,
252 source_occurrence: None,
253 })
254 }
255}
256
257fn word_prefixes_at(start: &BytesStart<'_>, inherited: &[String]) -> Result<Vec<String>> {
258 let mut prefixes = inherited.to_vec();
259 for attribute in start.attributes() {
260 let attribute = attribute?;
261 let name = attribute.key.as_ref();
262 let prefix = if name == b"xmlns" {
263 b"".as_slice()
264 } else if let Some(prefix) = name.strip_prefix(b"xmlns:") {
265 prefix
266 } else {
267 continue;
268 };
269 let prefix = std::str::from_utf8(prefix)?.to_string();
270 prefixes.retain(|candidate| candidate != &prefix);
271 let value =
272 attribute.decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())?;
273 if value.as_bytes() == W_NS.as_bytes() {
274 prefixes.push(prefix);
275 }
276 }
277 Ok(prefixes)
278}
279
280fn is_word_name(name: &[u8], word_prefixes: &[String]) -> bool {
281 let Some(separator) = name.iter().position(|byte| *byte == b':') else {
282 return word_prefixes.iter().any(String::is_empty);
283 };
284 word_prefixes
285 .iter()
286 .any(|prefix| prefix.as_bytes() == &name[..separator])
287}
288
289fn is_word_element(name: &[u8], local: &[u8], word_prefixes: &[String]) -> bool {
290 matches_local_name(name, local) && is_word_name(name, word_prefixes)
291}
292
293fn is_word_attribute(key: &[u8], local: &[u8], word_prefixes: &[String]) -> bool {
294 let Some(separator) = key.iter().position(|byte| *byte == b':') else {
295 return false;
296 };
297 key.get(separator + 1..) == Some(local)
298 && word_prefixes
299 .iter()
300 .any(|prefix| prefix.as_bytes() == &key[..separator])
301}
302
303#[derive(Debug, Clone, Default, PartialEq)]
305pub struct CT_Tabs {
306 pub tabs: Vec<CT_TabStop>,
307}
308
309impl CT_Tabs {
310 pub fn from_xml(reader: &mut Reader<&[u8]>) -> Result<Self> {
311 Self::from_xml_with_prefixes(reader, &["w".to_string()])
312 }
313
314 pub fn from_xml_with_prefixes(
316 reader: &mut Reader<&[u8]>,
317 word_prefixes: &[String],
318 ) -> Result<Self> {
319 let mut tabs = Vec::new();
320 let mut buf = Vec::new();
321 let mut scopes = vec![word_prefixes.to_vec()];
322
323 loop {
324 match reader.read_event_into(&mut buf) {
325 Ok(Event::Empty(ref e)) => {
326 let prefixes = word_prefixes_at(e, scopes.last().expect("outer scope"))?;
327 if scopes.len() == 1 && is_word_element(e.name().as_ref(), b"tab", &prefixes) {
328 let mut tab = CT_TabStop::from_xml_attrs_with_prefixes(e, &prefixes)?;
329 tab.source_occurrence = Some(tabs.len());
330 tabs.push(tab);
331 }
332 }
333 Ok(Event::Start(ref e)) => {
334 if scopes.len() >= MAX_TAB_XML_DEPTH {
335 return Err(OxmlError::InvalidValue(format!(
336 "tab XML depth exceeds {MAX_TAB_XML_DEPTH}"
337 )));
338 }
339 let prefixes = word_prefixes_at(e, scopes.last().expect("outer scope"))?;
340 if scopes.len() == 1 && is_word_element(e.name().as_ref(), b"tab", &prefixes) {
341 let mut tab = CT_TabStop::from_xml_attrs_with_prefixes(e, &prefixes)?;
342 tab.source_occurrence = Some(tabs.len());
343 tabs.push(tab);
344 }
345 scopes.push(prefixes);
346 }
347 Ok(Event::End(ref e)) => {
348 if scopes.len() == 1 && is_word_element(e.name().as_ref(), b"tabs", &scopes[0])
349 {
350 break;
351 }
352 if scopes.len() > 1 {
353 scopes.pop();
354 }
355 }
356 Ok(Event::Eof) => break,
357 Err(e) => return Err(e.into()),
358 _ => {}
359 }
360 buf.clear();
361 }
362
363 Ok(CT_Tabs { tabs })
364 }
365
366 pub fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
367 if self.tabs.is_empty() {
368 return Ok(());
369 }
370
371 writer.write_event(Event::Start(BytesStart::new("w:tabs")))?;
372
373 let mut buf = itoa::Buffer::new();
374 for tab in &self.tabs {
375 let mut e = BytesStart::new("w:tab");
376 e.push_attribute(("w:val", tab.val.to_str()));
377 e.push_attribute(("w:pos", buf.format(tab.pos.0)));
378 if let Some(leader) = tab.leader {
379 e.push_attribute(("w:leader", leader.to_str()));
380 }
381 writer.write_event(Event::Empty(e))?;
382 }
383
384 writer.write_event(Event::End(BytesEnd::new("w:tabs")))?;
385 Ok(())
386 }
387}
388
389#[cfg(test)]
390mod tests {
391 use super::*;
392
393 #[test]
394 fn round_trip_borders() {
395 let bdr = CT_PBdr {
396 top: Some(CT_BorderEdge {
397 val: ST_Border::Single,
398 sz: Some(4),
399 space: Some(1),
400 color: Some("000000".to_string()),
401 }),
402 bottom: Some(CT_BorderEdge {
403 val: ST_Border::Double,
404 sz: Some(6),
405 space: Some(2),
406 color: Some("FF0000".to_string()),
407 }),
408 ..Default::default()
409 };
410
411 let mut output = Vec::new();
412 let mut writer = Writer::new(&mut output);
413 bdr.to_xml(&mut writer).unwrap();
414 let xml = String::from_utf8(output).unwrap();
415
416 let full = xml.to_string();
417 let mut reader = Reader::from_str(&full);
418 reader.config_mut().trim_text(true);
419 let mut buf = Vec::new();
420 loop {
422 match reader.read_event_into(&mut buf) {
423 Ok(Event::Start(ref e)) if matches_local_name(e.name().as_ref(), b"pBdr") => {
424 break;
425 }
426 _ => {}
427 }
428 buf.clear();
429 }
430 let parsed = CT_PBdr::from_xml(&mut reader).unwrap();
431
432 assert_eq!(parsed.top.as_ref().unwrap().val, ST_Border::Single);
433 assert_eq!(parsed.top.as_ref().unwrap().sz, Some(4));
434 assert_eq!(parsed.bottom.as_ref().unwrap().val, ST_Border::Double);
435 assert!(parsed.left.is_none());
436 }
437
438 #[test]
439 fn round_trip_tabs() {
440 let tabs = CT_Tabs {
441 tabs: vec![
442 CT_TabStop {
443 val: ST_TabJc::Left,
444 pos: Twips(720),
445 leader: None,
446 source_occurrence: None,
447 },
448 CT_TabStop {
449 val: ST_TabJc::Center,
450 pos: Twips(4320),
451 leader: Some(ST_TabLeader::Dot),
452 source_occurrence: None,
453 },
454 CT_TabStop {
455 val: ST_TabJc::Right,
456 pos: Twips(8640),
457 leader: Some(ST_TabLeader::Hyphen),
458 source_occurrence: None,
459 },
460 ],
461 };
462
463 let mut output = Vec::new();
464 let mut writer = Writer::new(&mut output);
465 tabs.to_xml(&mut writer).unwrap();
466 let xml = String::from_utf8(output).unwrap();
467
468 let mut reader = Reader::from_str(&xml);
469 reader.config_mut().trim_text(true);
470 let mut buf = Vec::new();
471 loop {
472 match reader.read_event_into(&mut buf) {
473 Ok(Event::Start(ref e)) if matches_local_name(e.name().as_ref(), b"tabs") => {
474 break;
475 }
476 _ => {}
477 }
478 buf.clear();
479 }
480 let parsed = CT_Tabs::from_xml(&mut reader).unwrap();
481
482 assert_eq!(parsed.tabs.len(), 3);
483 assert_eq!(parsed.tabs[0].val, ST_TabJc::Left);
484 assert_eq!(parsed.tabs[0].pos, Twips(720));
485 assert_eq!(parsed.tabs[1].val, ST_TabJc::Center);
486 assert_eq!(parsed.tabs[1].leader, Some(ST_TabLeader::Dot));
487 assert_eq!(parsed.tabs[2].val, ST_TabJc::Right);
488 assert_eq!(parsed.tabs[0].source_occurrence, Some(0));
489 assert_eq!(parsed.tabs[1].source_occurrence, Some(1));
490 }
491
492 #[test]
493 fn namespace_aware_tabs_ignore_foreign_same_local_children() {
494 let xml = r#"<q:tabs xmlns:q="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:ext="urn:producer"><ext:tab ext:val="right" ext:pos="99"/><q:tab q:val="left" q:pos="720"/></q:tabs>"#;
495 let mut reader = Reader::from_str(xml);
496 let mut buf = Vec::new();
497 loop {
498 match reader.read_event_into(&mut buf) {
499 Ok(Event::Start(_)) => break,
500 Ok(Event::Eof) => panic!("missing tabs start"),
501 _ => {}
502 }
503 buf.clear();
504 }
505 let parsed = CT_Tabs::from_xml_with_prefixes(&mut reader, &["q".to_string()]).unwrap();
506 assert_eq!(parsed.tabs.len(), 1);
507 assert_eq!(parsed.tabs[0].pos, Twips(720));
508 assert_eq!(parsed.tabs[0].source_occurrence, Some(0));
509
510 let mut constructed = CT_TabStop::new(ST_TabJc::Left, Twips(720));
511 assert_eq!(parsed.tabs[0], constructed);
512 constructed.source_occurrence = Some(99);
513 assert_eq!(parsed.tabs[0], constructed);
514 }
515
516 #[test]
517 fn namespace_aware_tabs_track_shadows_and_expanded_tab_elements() {
518 let xml = r#"<q:tabs xmlns:q="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><foreign xmlns="urn:producer" xmlns:q="urn:producer"><q:tab q:val="right" q:pos="99"/><q:tabs><q:tab q:val="right" q:pos="100"/></q:tabs></foreign><q:tab q:val="left" q:pos="720"></q:tab><q:tab q:val="right" q:pos="1440"/></q:tabs>"#;
519 let mut reader = Reader::from_str(xml);
520 let mut buf = Vec::new();
521 loop {
522 match reader.read_event_into(&mut buf) {
523 Ok(Event::Start(_)) => break,
524 Ok(Event::Eof) => panic!("missing tabs start"),
525 event => {
526 event.unwrap();
527 }
528 }
529 buf.clear();
530 }
531
532 let parsed = CT_Tabs::from_xml_with_prefixes(&mut reader, &["q".to_string()]).unwrap();
533 assert_eq!(parsed.tabs.len(), 2);
534 assert_eq!(parsed.tabs[0].pos, Twips(720));
535 assert_eq!(parsed.tabs[0].source_occurrence, Some(0));
536 assert_eq!(parsed.tabs[1].pos, Twips(1440));
537 assert_eq!(parsed.tabs[1].source_occurrence, Some(1));
538
539 let default_xml = r#"<tabs xmlns="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><foreign xmlns="urn:producer"><tab val="right" pos="99"/><tabs><tab val="right" pos="100"/></tabs></foreign><tab xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" w:val="center" w:pos="2160"></tab></tabs>"#;
540 let mut reader = Reader::from_str(default_xml);
541 let mut buf = Vec::new();
542 loop {
543 match reader.read_event_into(&mut buf) {
544 Ok(Event::Start(_)) => break,
545 Ok(Event::Eof) => panic!("missing default tabs start"),
546 event => {
547 event.unwrap();
548 }
549 }
550 buf.clear();
551 }
552 let parsed = CT_Tabs::from_xml_with_prefixes(&mut reader, &[String::new()]).unwrap();
553 assert_eq!(parsed.tabs.len(), 1);
554 assert_eq!(parsed.tabs[0].val, ST_TabJc::Center);
555 assert_eq!(parsed.tabs[0].pos, Twips(2160));
556 assert_eq!(parsed.tabs[0].source_occurrence, Some(0));
557 }
558
559 #[test]
560 fn namespace_aware_tabs_reject_deep_distinct_aliases_normally() {
561 let mut xml = format!(r#"<q:tabs xmlns:q="{W_NS}">"#);
562 for depth in 0..MAX_TAB_XML_DEPTH {
563 xml.push_str(&format!(r#"<n{depth}:unknown xmlns:n{depth}="{W_NS}">"#));
564 }
565 for depth in (0..MAX_TAB_XML_DEPTH).rev() {
566 xml.push_str(&format!("</n{depth}:unknown>"));
567 }
568 xml.push_str("</q:tabs>");
569
570 let mut reader = Reader::from_str(&xml);
571 let mut buf = Vec::new();
572 loop {
573 match reader.read_event_into(&mut buf) {
574 Ok(Event::Start(_)) => break,
575 Ok(Event::Eof) => panic!("missing tabs start"),
576 event => {
577 event.unwrap();
578 }
579 }
580 buf.clear();
581 }
582 let error = CT_Tabs::from_xml_with_prefixes(&mut reader, &["q".to_string()])
583 .expect_err("deep alias nesting must be bounded");
584 assert!(error.to_string().contains("tab XML depth exceeds 64"));
585 }
586
587 #[test]
588 fn border_edge_all_styles_round_trip() {
589 let styles = [
591 ST_Border::None,
592 ST_Border::Single,
593 ST_Border::Thick,
594 ST_Border::Double,
595 ST_Border::Dotted,
596 ST_Border::Dashed,
597 ST_Border::DotDash,
598 ST_Border::Wave,
599 ];
600
601 for &style in &styles {
602 let bdr = CT_PBdr {
603 top: Some(CT_BorderEdge {
604 val: style,
605 sz: Some(8),
606 space: Some(0),
607 color: Some("FF00FF".to_string()),
608 }),
609 ..Default::default()
610 };
611
612 let mut output = Vec::new();
613 let mut writer = Writer::new(&mut output);
614 bdr.to_xml(&mut writer).unwrap();
615 let xml = String::from_utf8(output).unwrap();
616
617 let mut reader = Reader::from_str(&xml);
618 reader.config_mut().trim_text(true);
619 let mut buf = Vec::new();
620 loop {
621 match reader.read_event_into(&mut buf) {
622 Ok(Event::Start(ref e)) if matches_local_name(e.name().as_ref(), b"pBdr") => {
623 break;
624 }
625 _ => {}
626 }
627 buf.clear();
628 }
629 let parsed = CT_PBdr::from_xml(&mut reader).unwrap();
630 let top = parsed.top.as_ref().unwrap();
631 assert_eq!(
632 top.val, style,
633 "Border style round-trip failed for {style:?}"
634 );
635 assert_eq!(top.sz, Some(8));
636 assert_eq!(top.color.as_deref(), Some("FF00FF"));
637 }
638 }
639}