mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-13 11:54:50 +00:00
* craft-xml: a limit on the attributes a tree holds over all its elements max_attrs bounds one start tag and max_nodes counts elements and text, so attributes were the one thing nothing counted across the tree: 1,025 elements of 1,024 attributes each are 1,025 nodes and over a million (String, String) pairs, ten times the input in headers alone. Limits::max_attrs_total (default 4,000,000) bounds the attributes kept at once; elements handed to a streaming callback take theirs out of the count. Error::TooManyAttrsTotal. Ported from storytold/wordcraft#4320, which added the same budget to WordCraft's DOCX reader. * craft-xml tests: an internal DTD subset is refused wherever it hides, in linear time The cases of storytold/filmcraft#4139 (a marker inside a comment or a processing instruction, a '>' inside the system literal, 100,000 markers that never close) against Doctype::RejectInternalSubset. The reader already handles them; the tests keep it so. * craft-xml: El::node_count, for one node budget over the parts of a package max_nodes bounds one parse. A package of many parts, each inside the limit, still costs a tree per part the reader keeps. node_count is what max_nodes counted for an element, so a reader charges each tree against one budget and reads the next part with what is left as its max_nodes: a part over the budget stops before its tree is built. Ported from storytold/wordcraft#5665, which adds a package-wide element budget to WordCraft's DOCX reader (parse_counted with a max). * craft-xml: parse_streaming_partial returns what was read when a streaming read fails A worksheet whose rows cannot all be read (one row over the node limit, a callback error, markup the tokenizer cannot read) lost everything else in the part with it. parse_streaming_partial returns the error together with the root as far as it was read: the children complete before the failure and the elements open at that point. parse_streaming is unchanged. Ported from storytold/gridcraft#3317 (xml::parse_streaming_partial). * craft-xml: a strict pull reader refuses a second root and text outside the root Options::events builds no tree, so the checks the tree reader makes for a strict read (a second root element, character data outside the root) were not made at all: a strict pull over '<a/><b/>' or '<a/>text' ended without an error. A reader that walks events would take a damaged file's first element for the whole document. The reader now makes both checks itself, so the pull reader and the tree agree. Ported from storytold/wordcraft#1862, which made WordCraft's ODT event reader report malformed XML instead of stopping silently. --------- Co-authored-by: Oliwer Urbaniak <oliwerx12@gmail.com> Co-authored-by: Bra1nZ <95569966+Bra1nZ@users.noreply.github.com> Co-authored-by: Neil Cawse <neilcawse@geotab.com> Co-authored-by: midasmatterhorn <248507632+midasmatterhorn@users.noreply.github.com>
82 lines
3.7 KiB
Rust
82 lines
3.7 KiB
Rust
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
|
//! Bounded XML for hostile input.
|
|
//!
|
|
//! - [`parse`] and [`Options::parse`] read bytes into an owned tree of [`El`] and [`Node`], with
|
|
//! limits on input size, nesting, node count and attributes per element ([`Limits`]).
|
|
//! - [`Options::parse_streaming`] hands repeated elements to a callback, so a large part never
|
|
//! becomes a whole tree. [`Options::events`] is the pull reader underneath.
|
|
//! - [`Names`] chooses how names are read: as written, by local name, or rewritten to the
|
|
//! caller's canonical prefixes by namespace URI.
|
|
//! - [`esc`], [`esc_attr`], [`El::to_xml`], [`El::to_xml_pretty`] and [`Writer`] write XML.
|
|
//!
|
|
//! No document type definition is processed, and no entity is expanded except the five
|
|
//! predefined ones (`&` `<` `>` `"` `'`) and numeric character references.
|
|
//! The text the reader produces is therefore never more than three times its input.
|
|
//!
|
|
//! ```
|
|
//! let root = craft_xml::parse(b"<a x='1'><b>one & two</b><b/></a>")?;
|
|
//! assert_eq!(root.attr_u32("x"), Some(1));
|
|
//! assert_eq!(root.child("b").map(|b| b.text().into_owned()).as_deref(), Some("one & two"));
|
|
//! assert_eq!(root.to_xml(), r#"<a x="1"><b>one & two</b><b/></a>"#);
|
|
//! # Ok::<(), craft_xml::Error>(())
|
|
//! ```
|
|
|
|
#[cfg(doctest)]
|
|
#[doc = include_str!("../README.md")]
|
|
struct ReadmeExamples;
|
|
|
|
mod read;
|
|
mod tree;
|
|
pub mod value;
|
|
mod write;
|
|
|
|
pub use read::{
|
|
AttrValueHook, Canonical, Doctype, ElementHook, Event, Events, Limits, Names, NsName, Options, Scope, UnknownEntity, XML_NS, entity, parse,
|
|
to_utf8, unescape,
|
|
};
|
|
pub use tree::{Descendants, El, Local, Node, Pat};
|
|
pub use write::{DECL, DECL_STANDALONE, Tag, Writer, esc, esc_attr, is_name, is_xml_char, push_esc, push_esc_attr};
|
|
|
|
/// Why a document was not read.
|
|
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
|
|
pub enum Error {
|
|
/// The input is longer than [`Limits::max_input`].
|
|
#[error("the XML is larger than {0} bytes")]
|
|
TooLarge(usize),
|
|
/// Elements nest deeper than [`Limits::max_depth`].
|
|
#[error("XML elements nest deeper than {0} levels")]
|
|
TooDeep(usize),
|
|
/// The tree would hold more than [`Limits::max_nodes`] nodes.
|
|
#[error("the XML holds more than {0} nodes")]
|
|
TooManyNodes(usize),
|
|
/// One start tag has more than [`Limits::max_attrs`] attributes.
|
|
#[error("an XML element has more than {0} attributes")]
|
|
TooManyAttrs(usize),
|
|
/// The tree would hold more than [`Limits::max_attrs_total`] attributes.
|
|
#[error("the XML holds more than {0} attributes")]
|
|
TooManyAttrsTotal(usize),
|
|
/// The document has a document type declaration that [`Options::doctype`] refuses.
|
|
#[error("the XML has a document type declaration, which is not accepted")]
|
|
Doctype,
|
|
/// A reference to an entity that is neither predefined nor numeric, with
|
|
/// [`UnknownEntity::Error`].
|
|
#[error("the entity `&{name};` at byte {at} is not defined")]
|
|
UnknownEntity { at: usize, name: String },
|
|
/// Text that is not UTF-8, in a strict parse.
|
|
#[error("the XML is not valid UTF-8 at byte {0}")]
|
|
Encoding(usize),
|
|
/// Markup that cannot be read. `at` is a byte offset into the UTF-8 text ([`to_utf8`]).
|
|
#[error("malformed XML at byte {at}: {msg}")]
|
|
Syntax { at: usize, msg: String },
|
|
/// There is no root element.
|
|
#[error("the XML has no root element")]
|
|
NoRoot,
|
|
}
|
|
|
|
impl Error {
|
|
/// Was a limit exceeded (as opposed to the markup being wrong)?
|
|
pub fn is_limit(&self) -> bool {
|
|
matches!(self, Error::TooLarge(_) | Error::TooDeep(_) | Error::TooManyNodes(_) | Error::TooManyAttrs(_) | Error::TooManyAttrsTotal(_))
|
|
}
|
|
}
|