- 1
//! Bounded XML event walk over one part (O2). - 2
//! - 3
//! `quick-xml` never expands entities, so there is no expansion attack to - 4
//! defend against; a `DOCTYPE` is still refused, because no Open XML - 5
//! producer emits one and a part that carries one is not an honest part. - 6
- 7
use quick_xml::events::{BytesStart, Event}; - 8
- 9
use crate::{Error, Limits}; - 10
- 11
#[derive(Debug, Clone, PartialEq, Eq)] - 12
pub struct Element { - 13
/// Qualified name as written, e.g. `w:p`. - 14
pub name: String, - 15
pub attributes: Vec<(String, String)>, - 16
} - 17
- 18
impl Element { - 19
/// Local part of the element name (`p` for `w:p`). - 20
pub fn local(&self) -> &str { - 21
local_name(&self.name) - 22
} - 23
- 24
/// First attribute whose local name matches, with or without a prefix. - 25
pub fn attr(&self, local: &str) -> Option<&str> { - 26
self.attributes - 27
.iter() - 28
.find(|(name, _)| local_name(name) == local) - 29
.map(|(_, value)| value.as_str()) - 30
} - 31
- 32
/// Attribute with no prefix (`id` but not `r:id`). - 33
pub fn attr_unprefixed(&self, name: &str) -> Option<&str> { - 34
self.attributes - 35
.iter() - 36
.find(|(key, _)| key == name) - 37
.map(|(_, value)| value.as_str()) - 38
} - 39
- 40
/// Prefixed attribute by local name (`r:id` for `id`). - 41
pub fn attr_prefixed(&self, local: &str) -> Option<&str> { - 42
self.attributes - 43
.iter() - 44
.find(|(key, _)| key.contains(':') && local_name(key) == local) - 45
.map(|(_, value)| value.as_str()) - 46
} - 47
} - 48
- 49
pub fn local_name(qualified: &str) -> &str { - 50
qualified - 51
.rsplit_once(':') - 52
.map(|(_, local)| local) - 53
.unwrap_or(qualified) - 54
} - 55
- 56
#[derive(Debug, Clone, PartialEq, Eq)] - 57
pub enum XmlEvent { - 58
Open(Element), - 59
Close(String), - 60
Text(String), - 61
} - 62
- 63
/// Walks `bytes` as XML, calling `visit` for each element open, close and - 64
/// text run. Empty elements produce an `Open` immediately followed by a - 65
/// `Close`. Entity and character references are resolved into the text. - 66
/// - 67
/// Markup compatibility (`mc:AlternateContent`) is resolved the way a - 68
/// reader sees it: the first `mc:Choice` is visited, and later choices and - 69
/// the `mc:Fallback` are skipped (a fallback is visited only when there is - 70
/// no choice). Producers write the same content into both, so visiting both - 71
/// would read it twice. Skipped markup is still parsed and bounded. - 72
pub fn walk( - 73
bytes: &[u8], - 74
part: &str, - 75
limits: &Limits, - 76
mut visit: impl FnMut(XmlEvent) -> Result<(), Error>, - 77
) -> Result<(), Error> { - 78
if bytes.starts_with(&[0xFF, 0xFE]) || bytes.starts_with(&[0xFE, 0xFF]) { - 79
return Err(xml_error(part, "UTF-16 parts are not supported")); - 80
} - 81
let mut reader = quick_xml::Reader::from_reader(bytes); - 82
reader.config_mut().check_end_names = true; - 83
let mut buffer = Vec::new(); - 84
let mut depth = 0usize; - 85
let mut saw_root = false; - 86
// One entry per open AlternateContent: whether a Choice was taken. - 87
let mut alternates: Vec<bool> = Vec::new(); - 88
// Depth of the element whose subtree is being skipped. - 89
let mut skipping: Option<usize> = None; - 90
loop { - 91
let event = reader - 92
.read_event_into(&mut buffer) - 93
.map_err(|error| xml_error(part, &error.to_string()))?; - 94
match event { - 95
Event::Start(start) => { - 96
depth += 1; - 97
if depth > limits.max_xml_depth { - 98
return Err(Error::TooDeep(part.to_string())); - 99
} - 100
saw_root = true; - 101
let element = element_of(&start, part, limits)?; - 102
if skipping.is_none() { - 103
if skip_alternate(&element, &mut alternates) { - 104
skipping = Some(depth); - 105
} else { - 106
if element.local() == "AlternateContent" { - 107
alternates.push(false); - 108
} - 109
visit(XmlEvent::Open(element))?; - 110
} - 111
} - 112
} - 113
Event::Empty(start) => { - 114
if depth + 1 > limits.max_xml_depth { - 115
return Err(Error::TooDeep(part.to_string())); - 116
} - 117
saw_root = true; - 118
let element = element_of(&start, part, limits)?; - 119
if skipping.is_none() && !skip_alternate(&element, &mut alternates) { - 120
let name = element.name.clone(); - 121
visit(XmlEvent::Open(element))?; - 122
visit(XmlEvent::Close(name))?; - 123
} - 124
} - 125
Event::End(end) => { - 126
let name = String::from_utf8_lossy(end.name().as_ref()).into_owned(); - 127
match skipping { - 128
Some(level) if level == depth => skipping = None, - 129
Some(_) => {} - 130
None => { - 131
if local_name(&name) == "AlternateContent" { - 132
alternates.pop(); - 133
} - 134
visit(XmlEvent::Close(name))?; - 135
} - 136
} - 137
depth = depth.saturating_sub(1); - 138
} - 139
Event::Text(text) => { - 140
let text = text - 141
.decode() - 142
.map_err(|error| xml_error(part, &error.to_string()))?; - 143
if skipping.is_none() && !text.is_empty() { - 144
visit(XmlEvent::Text(text.into_owned()))?; - 145
} - 146
} - 147
Event::CData(data) => { - 148
let text = data - 149
.decode() - 150
.map_err(|error| xml_error(part, &error.to_string()))?; - 151
if skipping.is_none() { - 152
visit(XmlEvent::Text(text.into_owned()))?; - 153
} - 154
} - 155
Event::GeneralRef(reference) => { - 156
let resolved = match reference - 157
.resolve_char_ref() - 158
.map_err(|error| xml_error(part, &error.to_string()))? - 159
{ - 160
Some(character) => character.to_string(), - 161
None => { - 162
let name = reference - 163
.decode() - 164
.map_err(|error| xml_error(part, &error.to_string()))?; - 165
predefined_entity(&name) - 166
.ok_or_else(|| xml_error(part, &format!("undeclared entity &{name};")))? - 167
.to_string() - 168
} - 169
}; - 170
if skipping.is_none() { - 171
visit(XmlEvent::Text(resolved))?; - 172
} - 173
} - 174
Event::DocType(_) => return Err(Error::DocType(part.to_string())), - 175
Event::Eof => { - 176
if !saw_root { - 177
return Err(xml_error(part, "no root element")); - 178
} - 179
if depth != 0 { - 180
return Err(xml_error(part, "unexpected end of document")); - 181
} - 182
return Ok(()); - 183
} - 184
Event::Decl(_) | Event::PI(_) | Event::Comment(_) => {} - 185
} - 186
buffer.clear(); - 187
} - 188
} - 189
- 190
/// True when `element` is a `Choice` after the first, or a `Fallback` - 191
/// after a taken choice, inside the innermost open `AlternateContent`. - 192
/// Marks the first `Choice` as taken. - 193
fn skip_alternate(element: &Element, alternates: &mut [bool]) -> bool { - 194
let Some(taken) = alternates.last_mut() else { - 195
return false; - 196
}; - 197
match element.local() { - 198
"Choice" if *taken => true, - 199
"Choice" => { - 200
*taken = true; - 201
false - 202
} - 203
"Fallback" => *taken, - 204
_ => false, - 205
} - 206
} - 207
- 208
/// Root element of a part. Reads only up to the root, with the same - 209
/// `DOCTYPE` refusal and attribute bound as a full walk. - 210
pub fn root(bytes: &[u8], part: &str, limits: &Limits) -> Result<Element, Error> { - 211
if bytes.starts_with(&[0xFF, 0xFE]) || bytes.starts_with(&[0xFE, 0xFF]) { - 212
return Err(xml_error(part, "UTF-16 parts are not supported")); - 213
} - 214
let mut reader = quick_xml::Reader::from_reader(bytes); - 215
let mut buffer = Vec::new(); - 216
loop { - 217
match reader - 218
.read_event_into(&mut buffer) - 219
.map_err(|error| xml_error(part, &error.to_string()))? - 220
{ - 221
Event::Start(start) | Event::Empty(start) => return element_of(&start, part, limits), - 222
Event::DocType(_) => return Err(Error::DocType(part.to_string())), - 223
Event::Eof => return Err(xml_error(part, "no root element")), - 224
_ => {} - 225
} - 226
buffer.clear(); - 227
} - 228
} - 229
- 230
/// Element and attributes of a start tag, under the attribute bound. - 231
pub(crate) fn element_of( - 232
start: &BytesStart<'_>, - 233
part: &str, - 234
limits: &Limits, - 235
) -> Result<Element, Error> { - 236
let name = String::from_utf8_lossy(start.name().as_ref()).into_owned(); - 237
let mut attributes = Vec::new(); - 238
for attribute in start.attributes() { - 239
let attribute = attribute.map_err(|error| xml_error(part, &error.to_string()))?; - 240
if attributes.len() >= limits.max_attributes { - 241
return Err(Error::TooManyAttributes(part.to_string())); - 242
} - 243
let key = String::from_utf8_lossy(attribute.key.as_ref()).into_owned(); - 244
let value = attribute - 245
.normalized_value(quick_xml::XmlVersion::default()) - 246
.map_err(|error| xml_error(part, &error.to_string()))? - 247
.into_owned(); - 248
attributes.push((key, value)); - 249
} - 250
Ok(Element { name, attributes }) - 251
} - 252
- 253
fn predefined_entity(name: &str) -> Option<&'static str> { - 254
match name { - 255
"amp" => Some("&"), - 256
"lt" => Some("<"), - 257
"gt" => Some(">"), - 258
"quot" => Some("\""), - 259
"apos" => Some("'"), - 260
_ => None, - 261
} - 262
} - 263
- 264
fn xml_error(part: &str, message: &str) -> Error { - 265
Error::Xml { - 266
part: part.to_string(), - 267
message: message.to_string(), - 268
} - 269
} - 270
- 271
#[cfg(test)] - 272
#[allow(clippy::unwrap_used, clippy::expect_used)] - 273
mod tests { - 274
use super::*; - 275
- 276
fn collect(xml: &str) -> Result<Vec<XmlEvent>, Error> { - 277
let mut events = Vec::new(); - 278
walk(xml.as_bytes(), "t.xml", &Limits::default(), |event| { - 279
events.push(event); - 280
Ok(()) - 281
})?; - 282
Ok(events) - 283
} - 284
- 285
#[test] - 286
fn resolves_references_into_text() { - 287
let events = collect("<a>x & y A<</a>").unwrap(); - 288
let text: String = events - 289
.iter() - 290
.filter_map(|event| match event { - 291
XmlEvent::Text(text) => Some(text.as_str()), - 292
_ => None, - 293
}) - 294
.collect(); - 295
assert_eq!(text, "x & y A<"); - 296
} - 297
- 298
#[test] - 299
fn refuses_doctype_and_undeclared_entities() { - 300
let doctype = "<?xml version=\"1.0\"?><!DOCTYPE a [<!ENTITY x \"boom\">]><a>&x;</a>"; - 301
assert_eq!(collect(doctype), Err(Error::DocType("t.xml".into()))); - 302
assert!(matches!(collect("<a>&x;</a>"), Err(Error::Xml { .. }))); - 303
} - 304
- 305
#[test] - 306
fn enforces_depth_and_attribute_bounds() { - 307
let deep = format!("{}{}", "<a>".repeat(300), "</a>".repeat(300)); - 308
assert_eq!(collect(&deep), Err(Error::TooDeep("t.xml".into()))); - 309
let attributes: String = (0..300).map(|index| format!(" a{index}=\"1\"")).collect(); - 310
assert_eq!( - 311
collect(&format!("<a{attributes}/>")), - 312
Err(Error::TooManyAttributes("t.xml".into())) - 313
); - 314
} - 315
- 316
#[test] - 317
fn alternate_content_is_read_once() { - 318
let xml = r#"<r xmlns:mc="m"><mc:AlternateContent><mc:Choice Requires="x"><t>new</t></mc:Choice><mc:Choice Requires="y"><t>other</t></mc:Choice><mc:Fallback><t>old</t></mc:Fallback></mc:AlternateContent><mc:AlternateContent><mc:Fallback><t>only</t></mc:Fallback></mc:AlternateContent><t>after</t></r>"#; - 319
let text: Vec<String> = collect(xml) - 320
.unwrap() - 321
.into_iter() - 322
.filter_map(|event| match event { - 323
XmlEvent::Text(text) => Some(text), - 324
_ => None, - 325
}) - 326
.collect(); - 327
assert_eq!(text, vec!["new", "only", "after"]); - 328
let deep_skip = format!( - 329
"<r xmlns:mc=\"m\"><mc:AlternateContent><mc:Choice/><mc:Fallback>{}{}</mc:Fallback></mc:AlternateContent></r>", - 330
"<a>".repeat(300), - 331
"</a>".repeat(300) - 332
); - 333
assert_eq!(collect(&deep_skip), Err(Error::TooDeep("t.xml".into()))); - 334
} - 335
- 336
#[test] - 337
fn rejects_truncated_and_mismatched_markup() { - 338
assert!(collect("<a><b></a>").is_err()); - 339
assert!(collect("<a>").is_err()); - 340
assert!(collect("").is_err()); - 341
} - 342
} - 343
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.