use std::cell::Cell;
use std::collections::{HashMap, HashSet};
use std::fmt::Write as _;
use std::ops::Deref;
#[cfg(feature = "embedded")]
use crate::xml_input_shared as xml_sec_xml_input;
use super::budget::Meter;
use super::{BudgetKind, Document, Error, NodeId, NodeKind, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OutputMethod {
#[default]
Xml,
Html,
Text,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OutputDefinition {
pub method: OutputMethod,
pub version: Option<String>,
pub encoding: String,
pub omit_xml_declaration: bool,
pub standalone: Option<bool>,
pub doctype_public: Option<String>,
pub doctype_system: Option<String>,
pub indent: bool,
pub media_type: Option<String>,
pub cdata_section_elements: HashSet<super::ExpandedName>,
pub(crate) method_explicit: bool,
pub(crate) encoding_explicit: bool,
pub(crate) indent_explicit: bool,
pub(crate) inject_content_type: bool,
}
impl Default for OutputDefinition {
fn default() -> Self {
Self {
method: OutputMethod::Xml,
version: Some("1.0".into()),
encoding: "UTF-8".into(),
omit_xml_declaration: false,
standalone: None,
doctype_public: None,
doctype_system: None,
indent: false,
media_type: None,
cdata_section_elements: HashSet::new(),
method_explicit: false,
encoding_explicit: false,
indent_explicit: false,
inject_content_type: true,
}
}
}
impl OutputDefinition {
pub(crate) fn owned_bytes(&self) -> usize {
self.version
.as_ref()
.map_or(0, String::len)
.saturating_add(self.encoding.len())
.saturating_add(self.doctype_public.as_ref().map_or(0, String::len))
.saturating_add(self.doctype_system.as_ref().map_or(0, String::len))
.saturating_add(self.media_type.as_ref().map_or(0, String::len))
.saturating_add(super::budget::retained_hash_storage::<super::ExpandedName>(
self.cdata_section_elements.capacity(),
))
.saturating_add(
self.cdata_section_elements
.iter()
.fold(0usize, |total, name| {
total
.saturating_add(name.namespace.as_ref().map_or(0, String::len))
.saturating_add(name.local.len())
}),
)
}
}
struct EffectiveOutputDefinition<'a> {
source: &'a OutputDefinition,
method: OutputMethod,
indent: bool,
xml_version: &'a str,
}
impl Deref for EffectiveOutputDefinition<'_> {
type Target = OutputDefinition;
fn deref(&self) -> &Self::Target {
self.source
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SerializedOutput {
pub bytes: Vec<u8>,
pub encoding: String,
pub media_type: Option<String>,
}
enum OutputEncoding {
Utf8,
Utf16,
Utf16Le,
Utf16Be,
Ascii,
Registered(xml_sec_xml_input::IanaSingleByteEncoding),
Other {
encoding: &'static encoding_rs::Encoding,
representable: [Cell<Option<(char, bool)>>; 16],
},
}
impl OutputEncoding {
fn new(label: &str) -> Result<Self> {
if label.eq_ignore_ascii_case("utf-8") {
Ok(Self::Utf8)
} else if label.eq_ignore_ascii_case("utf-16") {
Ok(Self::Utf16)
} else if label.eq_ignore_ascii_case("utf-16le") {
Ok(Self::Utf16Le)
} else if label.eq_ignore_ascii_case("utf-16be") {
Ok(Self::Utf16Be)
} else if is_ascii_encoding_label(label) {
Ok(Self::Ascii)
} else if let Some(encoding) = xml_sec_xml_input::registered_single_byte_encoding(label) {
Ok(Self::Registered(encoding))
} else {
encoding_rs::Encoding::for_label_no_replacement(label.as_bytes())
.and_then(|encoding| {
xml_sec_xml_input::legacy_label_matches_encoding(label, encoding).then_some(
Self::Other {
encoding,
representable: [const { Cell::new(None) }; 16],
},
)
})
.ok_or_else(|| Error::Serialization(format!("unsupported output encoding {label}")))
}
}
fn represents(&self, character: char) -> bool {
match self {
Self::Utf8 | Self::Utf16 | Self::Utf16Le | Self::Utf16Be => true,
Self::Ascii => character.is_ascii(),
Self::Registered(encoding) => encoding.encode_char(character).is_some(),
Self::Other {
encoding,
representable,
} => {
let slot = &representable[u32::from(character) as usize % representable.len()];
if let Some((cached, value)) = slot.get()
&& cached == character
{
return value;
}
let value = encoding_represents(encoding, character);
slot.set(Some((character, value)));
value
}
}
}
fn canonical_name(&self) -> &'static str {
match self {
Self::Utf8 => "UTF-8",
Self::Utf16 => "UTF-16",
Self::Utf16Le => "UTF-16LE",
Self::Utf16Be => "UTF-16BE",
Self::Ascii => "US-ASCII",
Self::Registered(encoding) => encoding.name(),
Self::Other { encoding, .. } => encoding.name(),
}
}
fn declaration_name<'a>(&self, requested: &'a str) -> &'a str {
if matches!(self, Self::Ascii) {
if requested.eq_ignore_ascii_case("ascii") || requested.eq_ignore_ascii_case("us-ascii")
{
requested
} else {
self.canonical_name()
}
} else if xml_sec_xml_input::is_xml_encoding_name(requested) {
requested
} else {
self.canonical_name()
}
}
}
fn encoding_represents(encoding: &'static encoding_rs::Encoding, character: char) -> bool {
let mut utf8 = [0_u8; 4];
let mut source: &str = character.encode_utf8(&mut utf8);
let mut encoder = encoding.new_encoder();
let mut output = [0_u8; 32];
loop {
let (result, read, _) =
encoder.encode_from_utf8_without_replacement(source, &mut output, true);
source = &source[read..];
match result {
encoding_rs::EncoderResult::InputEmpty => return true,
encoding_rs::EncoderResult::OutputFull => {}
encoding_rs::EncoderResult::Unmappable(_) => return false,
}
}
}
fn is_ascii_encoding_label(label: &str) -> bool {
[
"us-ascii",
"ascii",
"ansi_x3.4-1968",
"ansi_x3.4-1986",
"iso-ir-6",
"iso_646.irv:1991",
"iso646-us",
"ibm367",
"cp367",
"csascii",
"us",
]
.iter()
.any(|alias| label.eq_ignore_ascii_case(alias))
}
pub(crate) fn serialize(
document: &Document,
definition: &OutputDefinition,
meter: &mut Meter,
) -> Result<SerializedOutput> {
serialize_charged(document, definition, meter, BudgetKind::SerializedBytes)
}
pub(crate) fn serialize_fragment(document: &Document, meter: &mut Meter) -> Result<String> {
let mut definition = OutputDefinition {
omit_xml_declaration: true,
..OutputDefinition::default()
};
definition.method_explicit = true;
definition.indent_explicit = true;
let definition = EffectiveOutputDefinition {
method: definition.method,
indent: definition.indent,
xml_version: supported_xml_version(&definition),
source: &definition,
};
let (used, limit) = meter.usage(BudgetKind::OwnedBytes)?;
let mut counter = RenderBuffer::counting(
EncodingCounter::new(&OutputEncoding::Utf8),
BudgetKind::OwnedBytes,
used,
limit,
);
render(
document,
&definition,
&OutputEncoding::Utf8,
&mut counter,
meter,
)?;
let bytes = counter.len();
meter.charge(BudgetKind::OwnedBytes, bytes)?;
let mut text = RenderBuffer::Text(String::with_capacity(bytes));
render(
document,
&definition,
&OutputEncoding::Utf8,
&mut text,
meter,
)?;
Ok(text.into_string())
}
fn serialize_charged(
document: &Document,
definition: &OutputDefinition,
meter: &mut Meter,
budget_kind: BudgetKind,
) -> Result<SerializedOutput> {
let infer_html_method = !definition.method_explicit && infer_html_output(document);
let infer_indent = (infer_html_method || definition.method == OutputMethod::Html)
&& !definition.indent_explicit;
let effective = EffectiveOutputDefinition {
source: definition,
method: if infer_html_method {
OutputMethod::Html
} else {
definition.method
},
indent: infer_indent || definition.indent,
xml_version: supported_xml_version(definition),
};
serialize_with_definition(document, &effective, meter, budget_kind)
}
fn serialize_with_definition(
document: &Document,
definition: &EffectiveOutputDefinition<'_>,
meter: &mut Meter,
budget_kind: BudgetKind,
) -> Result<SerializedOutput> {
if definition.method == OutputMethod::Xml
&& ((!definition.omit_xml_declaration && definition.standalone.is_some())
|| definition.doctype_system.is_some())
{
validate_document_output_shape(document)?;
}
let encoding = OutputEncoding::new(&definition.encoding)?;
let (used, limit) = meter.usage(budget_kind)?;
let mut counter =
RenderBuffer::counting(EncodingCounter::new(&encoding), budget_kind, used, limit);
render(document, definition, &encoding, &mut counter, meter)?;
let text_bytes = counter.len();
let encoded_bytes = counter.encoded_len()?;
meter.check_additional(budget_kind, encoded_bytes)?;
meter.charge(BudgetKind::OwnedBytes, text_bytes)?;
let mut text = RenderBuffer::Text(String::with_capacity(text_bytes));
render(document, definition, &encoding, &mut text, meter)?;
let text = text.into_string();
if definition.method == OutputMethod::Xml {
validate_xml_characters(&text, definition.xml_version)?;
}
if definition.method == OutputMethod::Text {
validate_text_encoding(&text, &encoding, &definition.encoding)?;
} else {
reject_unrepresentable_markup(&text, &encoding, "serialized output")?;
}
let bytes = encode(
text,
text_bytes,
encoded_bytes,
&encoding,
meter,
budget_kind,
)?;
let media_type = definition
.media_type
.as_deref()
.unwrap_or(match definition.method {
OutputMethod::Xml => "text/xml",
OutputMethod::Html => "text/html",
OutputMethod::Text => "text/plain",
});
meter.charge(
BudgetKind::OwnedBytes,
definition.encoding.len().saturating_add(media_type.len()),
)?;
Ok(SerializedOutput {
bytes,
encoding: definition.encoding.clone(),
media_type: Some(media_type.into()),
})
}
fn supported_xml_version(definition: &OutputDefinition) -> &str {
match definition.version.as_deref() {
Some(version @ ("1.0" | "1.1")) => version,
_ => "1.0",
}
}
fn render(
document: &Document,
definition: &EffectiveOutputDefinition<'_>,
encoding: &OutputEncoding,
text: &mut RenderBuffer,
meter: &mut Meter,
) -> Result<()> {
if definition.method == OutputMethod::Xml && !definition.omit_xml_declaration {
text.push_str("<?xml version=\"");
text.push_str(definition.xml_version);
text.push('"');
if definition.encoding_explicit {
text.push_str(" encoding=\"");
text.push_str(encoding.declaration_name(&definition.encoding));
text.push('"');
}
if let Some(standalone) = definition.standalone {
text.push_str(if standalone {
" standalone=\"yes\""
} else {
" standalone=\"no\""
});
}
text.push_str("?>");
text.push('\n');
}
let children = &document
.node(document.root())
.ok_or_else(|| Error::Serialization("missing result root".into()))?
.children;
let document_element = children.iter().copied().find(|child| {
document
.node(*child)
.is_some_and(|node| matches!(node.kind, NodeKind::Element { .. }))
});
for (index, child) in children.iter().enumerate() {
if definition.method != OutputMethod::Text && Some(*child) == document_element {
render_doctype(document, *child, definition, encoding, text)?;
}
serialize_node(
document,
*child,
definition,
encoding,
text,
RenderContext::root(),
meter,
)?;
if definition.method == OutputMethod::Xml
&& (definition.indent || !definition.omit_xml_declaration)
&& index + 1 < children.len()
&& document
.node(*child)
.is_some_and(|node| matches!(node.kind, NodeKind::Comment(_)))
{
text.push('\n');
}
}
if definition.method != OutputMethod::Text && (!definition.indent_explicit || definition.indent)
{
text.push('\n');
}
Ok(())
}
fn render_doctype(
document: &Document,
element: NodeId,
definition: &EffectiveOutputDefinition<'_>,
encoding: &OutputEncoding,
text: &mut RenderBuffer,
) -> Result<()> {
if definition.doctype_system.is_none()
&& !(definition.method == OutputMethod::Html && definition.doctype_public.is_some())
{
return Ok(());
}
if definition.method == OutputMethod::Xml
&& let Some(public) = &definition.doctype_public
{
validate_xml_public_identifier(public)?;
}
if definition.method == OutputMethod::Xml
&& let Some(system) = &definition.doctype_system
{
validate_xml_system_identifier(system)?;
}
let Some(NodeKind::Element { name, prefix, .. }) =
document.node(element).map(|node| &node.kind)
else {
return Err(Error::Serialization(
"document element disappeared during serialization".into(),
));
};
text.push_str("<!DOCTYPE ");
if definition.method == OutputMethod::Html {
text.push_str("html");
} else {
push_name(prefix.as_deref(), &name.local, encoding, text)?;
}
match (&definition.doctype_public, &definition.doctype_system) {
(Some(public), Some(system)) => {
text.push_str(" PUBLIC ");
push_external_identifier_literal(public, "public identifier", encoding, text)?;
text.push(' ');
push_external_identifier_literal(system, "system identifier", encoding, text)?;
}
(Some(public), None) if definition.method == OutputMethod::Html => {
text.push_str(" PUBLIC ");
push_external_identifier_literal(public, "public identifier", encoding, text)?;
}
(Some(_), None) => {}
(None, Some(system)) => {
text.push_str(" SYSTEM ");
push_external_identifier_literal(system, "system identifier", encoding, text)?;
}
(None, None) => {}
}
text.push('>');
text.push('\n');
Ok(())
}
fn validate_xml_public_identifier(value: &str) -> Result<()> {
if value.chars().all(|character| {
character.is_ascii_alphanumeric()
|| matches!(
character,
' ' | '\r'
| '\n'
| '-'
| '\''
| '('
| ')'
| '+'
| ','
| '.'
| '/'
| ':'
| '='
| '?'
| ';'
| '!'
| '*'
| '#'
| '@'
| '$'
| '_'
| '%'
)
}) {
Ok(())
} else {
Err(Error::Serialization(
"doctype public identifier contains a character outside XML PubidChar".into(),
))
}
}
fn validate_xml_system_identifier(value: &str) -> Result<()> {
if value.contains('#') {
return Err(Error::Serialization(
"doctype system identifier must not contain a fragment identifier".into(),
));
}
Ok(())
}
fn push_external_identifier_literal(
value: &str,
kind: &str,
encoding: &OutputEncoding,
output: &mut RenderBuffer,
) -> Result<()> {
reject_unrepresentable_markup(value, encoding, kind)?;
let delimiter = if !value.contains('"') {
'"'
} else if !value.contains('\'') {
'\''
} else {
return Err(Error::Serialization(format!(
"doctype {kind} contains both quote delimiters"
)));
};
output.push(delimiter);
output.push_str(value);
output.push(delimiter);
Ok(())
}
enum RenderBuffer {
Count {
counter: EncodingCounter,
kind: BudgetKind,
used: usize,
limit: usize,
exceeded: Option<usize>,
},
Text(String),
}
impl RenderBuffer {
fn counting(counter: EncodingCounter, kind: BudgetKind, used: usize, limit: usize) -> Self {
Self::Count {
counter,
kind,
used,
limit,
exceeded: None,
}
}
fn push_str(&mut self, value: &str) {
match self {
Self::Count {
counter,
used,
limit,
exceeded,
..
} => {
if exceeded.is_some() {
return;
}
counter.push_str(value);
let actual = used.saturating_add(counter.encoded_bytes);
if actual > *limit {
*exceeded = Some(actual);
}
}
Self::Text(output) => output.push_str(value),
}
}
fn push(&mut self, value: char) {
match self {
Self::Count { .. } => {
let mut buffer = [0_u8; 4];
self.push_str(value.encode_utf8(&mut buffer));
}
Self::Text(output) => output.push(value),
}
}
fn push_repeated(&mut self, byte: u8, count: usize) -> Result<()> {
const CHUNK_SIZE: usize = 64;
let chunk = [byte; CHUNK_SIZE];
let chunk = std::str::from_utf8(&chunk)
.map_err(|_| Error::Serialization("non-ASCII repeated render byte".into()))?;
let mut remaining = count;
while remaining >= CHUNK_SIZE {
self.push_str(chunk);
self.ensure_within_limit()?;
remaining -= CHUNK_SIZE;
}
if remaining > 0 {
self.push_str(&chunk[..remaining]);
self.ensure_within_limit()?;
}
Ok(())
}
fn ensure_within_limit(&self) -> Result<()> {
if let Self::Count {
kind,
limit,
exceeded: Some(actual),
..
} = self
{
return Err(Error::Budget {
kind: *kind,
limit: *limit,
actual: *actual,
});
}
Ok(())
}
const fn len(&self) -> usize {
match self {
Self::Count { counter, .. } => counter.utf8_bytes,
Self::Text(output) => output.len(),
}
}
fn into_string(self) -> String {
match self {
Self::Text(output) => output,
Self::Count { .. } => unreachable!("counting buffer does not contain rendered text"),
}
}
fn encoded_len(&mut self) -> Result<usize> {
match self {
Self::Count {
counter,
used,
limit,
exceeded,
..
} => {
let encoded = counter.finish();
let actual = used.saturating_add(encoded);
if actual > *limit {
*exceeded = Some(actual);
}
self.ensure_within_limit()?;
Ok(encoded)
}
Self::Text(output) => Ok(output.len()),
}
}
}
impl std::fmt::Write for RenderBuffer {
fn write_str(&mut self, value: &str) -> std::fmt::Result {
self.push_str(value);
Ok(())
}
}
enum EncodingCounterKind {
Utf8,
Ascii,
Registered,
Utf16,
Other(encoding_rs::Encoder),
}
struct EncodingCounter {
utf8_bytes: usize,
encoded_bytes: usize,
kind: EncodingCounterKind,
finished: bool,
}
impl EncodingCounter {
fn new(encoding: &OutputEncoding) -> Self {
let kind = match encoding {
OutputEncoding::Utf8 => EncodingCounterKind::Utf8,
OutputEncoding::Ascii => EncodingCounterKind::Ascii,
OutputEncoding::Registered(_) => EncodingCounterKind::Registered,
OutputEncoding::Utf16 | OutputEncoding::Utf16Le | OutputEncoding::Utf16Be => {
EncodingCounterKind::Utf16
}
OutputEncoding::Other { encoding, .. } => {
EncodingCounterKind::Other(encoding.new_encoder())
}
};
let encoded_bytes = matches!(encoding, OutputEncoding::Utf16)
.then_some(2)
.unwrap_or(0);
Self {
utf8_bytes: 0,
encoded_bytes,
kind,
finished: false,
}
}
fn push_str(&mut self, value: &str) {
self.utf8_bytes = self.utf8_bytes.saturating_add(value.len());
match &mut self.kind {
EncodingCounterKind::Utf8 => {
self.encoded_bytes = self.encoded_bytes.saturating_add(value.len())
}
EncodingCounterKind::Ascii | EncodingCounterKind::Registered => {
self.encoded_bytes = self.encoded_bytes.saturating_add(value.chars().count());
}
EncodingCounterKind::Utf16 => {
self.encoded_bytes = self
.encoded_bytes
.saturating_add(value.encode_utf16().count().saturating_mul(2));
}
EncodingCounterKind::Other(encoder) => {
self.encoded_bytes = self
.encoded_bytes
.saturating_add(count_encoded(encoder, value, false));
}
}
}
fn finish(&mut self) -> usize {
if !self.finished {
if let EncodingCounterKind::Other(encoder) = &mut self.kind {
self.encoded_bytes = self
.encoded_bytes
.saturating_add(count_encoded(encoder, "", true));
}
self.finished = true;
}
self.encoded_bytes
}
}
fn count_encoded(encoder: &mut encoding_rs::Encoder, mut source: &str, last: bool) -> usize {
let mut total = 0usize;
let mut buffer = [0_u8; 4096];
loop {
let (result, read, written, _) = encoder.encode_from_utf8(source, &mut buffer, last);
total = total.saturating_add(written);
source = &source[read..];
match result {
encoding_rs::CoderResult::InputEmpty => break,
encoding_rs::CoderResult::OutputFull => {}
}
}
total
}
#[derive(Clone)]
struct RenderContext {
depth: usize,
parent: Option<NodeId>,
parent_mixed: bool,
html_whitespace_sensitive: bool,
}
struct NamespaceRollback<'document> {
prefix: Option<&'document str>,
previous_uri: Option<&'document str>,
}
impl RenderContext {
fn root() -> Self {
Self {
depth: 0,
parent: None,
parent_mixed: false,
html_whitespace_sensitive: false,
}
}
}
enum RenderTask {
Node(NodeId, RenderContext),
CdataRange {
parent: NodeId,
start: usize,
end: usize,
},
Children {
parent: NodeId,
next: usize,
context: RenderContext,
cdata: bool,
skip_legacy_content_type: bool,
},
CloseElement {
id: NodeId,
depth: usize,
mixed: bool,
parent_mixed: bool,
html_whitespace_sensitive: bool,
has_element_child: bool,
html_void: bool,
namespace_rollback_len: usize,
},
}
struct RenderWorkspace<'document> {
tasks: Vec<RenderTask>,
active_namespaces: HashMap<Option<&'document str>, &'document str>,
namespace_rollbacks: Vec<NamespaceRollback<'document>>,
reserved_owned_bytes: usize,
namespace_index_bytes: usize,
}
impl<'document> RenderWorkspace<'document> {
fn new() -> Self {
Self {
tasks: Vec::new(),
active_namespaces: HashMap::new(),
namespace_rollbacks: Vec::new(),
reserved_owned_bytes: 0,
namespace_index_bytes: 0,
}
}
fn push(&mut self, task: RenderTask, meter: &mut Meter) -> Result<()> {
super::budget::reserve_temporary_vec_slot(
&mut self.tasks,
meter,
&mut self.reserved_owned_bytes,
)?;
self.tasks.push(task);
Ok(())
}
fn bind_namespace(
&mut self,
prefix: Option<&'document str>,
uri: &'document str,
meter: &mut Meter,
) -> Result<()> {
super::budget::reserve_temporary_vec_slot(
&mut self.namespace_rollbacks,
meter,
&mut self.reserved_owned_bytes,
)?;
if !self.active_namespaces.contains_key(&prefix) {
super::budget::reserve_retained_hash_map_slot(
&mut self.active_namespaces,
meter,
&mut self.namespace_index_bytes,
)?;
}
let previous_uri = self.active_namespaces.insert(prefix, uri);
self.namespace_rollbacks.push(NamespaceRollback {
prefix,
previous_uri,
});
Ok(())
}
fn rollback_namespaces(&mut self, checkpoint: usize) {
while self.namespace_rollbacks.len() > checkpoint {
let rollback = self
.namespace_rollbacks
.pop()
.expect("namespace rollback checkpoint is valid");
if let Some(uri) = rollback.previous_uri {
self.active_namespaces.insert(rollback.prefix, uri);
} else {
self.active_namespaces.remove(&rollback.prefix);
}
}
}
}
fn serialize_node(
document: &Document,
id: NodeId,
definition: &EffectiveOutputDefinition<'_>,
encoding: &OutputEncoding,
output: &mut RenderBuffer,
context: RenderContext,
meter: &mut Meter,
) -> Result<()> {
let mut workspace = RenderWorkspace::new();
workspace.push(RenderTask::Node(id, context), meter)?;
let result = serialize_node_tasks(
document,
definition,
encoding,
output,
meter,
&mut workspace,
);
meter.release_owned_bytes(
workspace
.reserved_owned_bytes
.saturating_add(workspace.namespace_index_bytes),
);
result
}
fn serialize_node_tasks<'document>(
document: &'document Document,
definition: &EffectiveOutputDefinition<'_>,
encoding: &OutputEncoding,
output: &mut RenderBuffer,
meter: &mut Meter,
workspace: &mut RenderWorkspace<'document>,
) -> Result<()> {
while let Some(task) = workspace.tasks.pop() {
output.ensure_within_limit()?;
if let RenderTask::Children {
parent,
mut next,
context,
cdata,
skip_legacy_content_type,
} = task
{
let node = document
.node(parent)
.ok_or_else(|| Error::Serialization("result element disappeared".into()))?;
while next < node.children.len()
&& skip_legacy_content_type
&& document
.node(node.children[next])
.is_some_and(is_replaceable_legacy_content_type_meta)
{
next += 1;
}
if next == node.children.len() {
continue;
}
let child = node.children[next];
if cdata
&& matches!(
document.node(child).map(|node| &node.kind),
Some(NodeKind::Text {
disable_output_escaping: false,
..
})
)
{
let start = next;
next += 1;
while let Some(child) = node.children.get(next)
&& matches!(
document.node(*child).map(|node| &node.kind),
Some(NodeKind::Text {
disable_output_escaping: false,
..
})
)
{
next += 1;
}
workspace.push(
RenderTask::Children {
parent,
next,
context,
cdata,
skip_legacy_content_type,
},
meter,
)?;
workspace.push(
RenderTask::CdataRange {
parent,
start,
end: next,
},
meter,
)?;
continue;
}
workspace.push(
RenderTask::Children {
parent,
next: next + 1,
context: context.clone(),
cdata,
skip_legacy_content_type,
},
meter,
)?;
workspace.push(RenderTask::Node(child, context), meter)?;
continue;
}
if let RenderTask::CdataRange { parent, start, end } = task {
push_cdata_range(
document,
parent,
start,
end,
definition.xml_version,
encoding,
output,
)?;
continue;
}
if let RenderTask::CloseElement {
id,
depth,
mixed,
parent_mixed,
html_whitespace_sensitive,
has_element_child,
html_void,
namespace_rollback_len,
} = task
{
if definition.indent
&& !mixed
&& !parent_mixed
&& !html_whitespace_sensitive
&& has_element_child
{
output.push('\n');
output.push_repeated(b' ', depth.saturating_mul(2))?;
}
if !html_void {
let Some(NodeKind::Element { name, prefix, .. }) =
document.node(id).map(|node| &node.kind)
else {
return Err(Error::Serialization(
"result element disappeared during serialization".into(),
));
};
output.push_str("</");
push_name(prefix.as_deref(), &name.local, encoding, output)?;
output.push('>');
}
workspace.rollback_namespaces(namespace_rollback_len);
continue;
}
let RenderTask::Node(id, context) = task else {
unreachable!("render task variants were handled above")
};
let node = document
.node(id)
.ok_or_else(|| Error::Serialization("invalid result node".into()))?;
match &node.kind {
NodeKind::Root => {
workspace.push(
RenderTask::Children {
parent: id,
next: 0,
context,
cdata: false,
skip_legacy_content_type: false,
},
meter,
)?;
}
NodeKind::Text {
value,
disable_output_escaping,
} => {
let parent_name = context.parent.and_then(|parent| {
document.node(parent).and_then(|node| match &node.kind {
NodeKind::Element { name, .. } => Some(name),
_ => None,
})
});
if definition.method == OutputMethod::Text
|| parent_name.is_some_and(|name| {
definition.method == OutputMethod::Html
&& name.namespace.is_none()
&& ["script", "style"]
.iter()
.any(|candidate| name.local.eq_ignore_ascii_case(candidate))
})
{
if definition.method == OutputMethod::Html {
reject_unrepresentable_markup(value, encoding, "HTML raw text")?;
}
output.push_str(value);
} else if *disable_output_escaping {
if definition.method == OutputMethod::Xml {
validate_xml_characters(value, definition.xml_version)?;
}
push_xml_raw_text(value, definition.xml_version, encoding, output);
} else if definition.method == OutputMethod::Xml
&& parent_name
.is_some_and(|name| definition.cdata_section_elements.contains(name))
{
validate_xml_characters(value, definition.xml_version)?;
push_cdata(value, definition.xml_version, encoding, output);
} else {
escape_text(
value,
definition.xml_version,
encoding,
definition.method == OutputMethod::Xml,
output,
)?;
}
}
NodeKind::Comment(value) if definition.method != OutputMethod::Text => {
reject_xml11_restricted_markup(value, definition, "comment")?;
reject_unrepresentable_markup(value, encoding, "comment")?;
output.push_str("<!--");
output.push_str(value);
output.push_str("-->");
}
NodeKind::ProcessingInstruction { target, value }
if definition.method != OutputMethod::Text =>
{
if let Some(value) = value {
reject_xml11_restricted_markup(value, definition, "processing instruction")?;
reject_unrepresentable_markup(value, encoding, "processing instruction")?;
}
reject_unrepresentable_markup(target, encoding, "processing-instruction target")?;
output.push_str("<?");
output.push_str(target);
if let Some(value) = value {
output.push(' ');
output.push_str(value);
}
output.push_str(if definition.method == OutputMethod::Html {
">"
} else {
"?>"
});
}
NodeKind::Element { .. } if definition.method == OutputMethod::Text => {
workspace.push(
RenderTask::Children {
parent: id,
next: 0,
context,
cdata: false,
skip_legacy_content_type: false,
},
meter,
)?;
}
NodeKind::Element {
name,
prefix,
attributes,
namespaces,
} if definition.method != OutputMethod::Text => {
let mixed = node.children.iter().any(|child| {
matches!(
document.node(*child).map(|child| &child.kind),
Some(NodeKind::Text { value, .. }) if !value.is_empty()
)
});
if definition.indent
&& context.depth > 0
&& !context.parent_mixed
&& !context.html_whitespace_sensitive
{
output.push('\n');
output.push_repeated(b' ', context.depth.saturating_mul(2))?;
}
output.push('<');
push_name(prefix.as_deref(), &name.local, encoding, output)?;
let namespace_rollback_len = workspace.namespace_rollbacks.len();
let inherited_default_namespace = workspace.active_namespaces.get(&None).copied();
let inherits_empty_default_namespace =
inherited_default_namespace.is_none_or(str::is_empty);
if name.namespace.is_none()
&& inherited_default_namespace.is_some_and(|uri| !uri.is_empty())
&& !namespaces
.iter()
.any(|namespace| namespace.prefix.is_none() && namespace.uri.is_empty())
{
output.push_str(" xmlns=\"\"");
workspace.bind_namespace(None, "", meter)?;
}
let element_namespace_index = prefix.as_ref().and_then(|_| {
namespaces.iter().position(|namespace| {
namespace.prefix.as_deref() == prefix.as_deref()
&& Some(namespace.uri.as_str()) == name.namespace.as_deref()
})
});
let ordered_namespaces = element_namespace_index
.into_iter()
.map(|index| (index, &namespaces[index]))
.chain(
namespaces
.iter()
.enumerate()
.filter_map(|(index, namespace)| {
(Some(index) != element_namespace_index)
.then_some((index, namespace))
}),
);
for (_namespace_index, namespace) in ordered_namespaces {
if namespace.prefix.as_deref() == Some("xml")
&& namespace.uri == "http://www.w3.org/XML/1998/namespace"
{
continue;
}
if namespace.prefix.is_none()
&& namespace.uri.is_empty()
&& inherits_empty_default_namespace
{
continue;
}
if workspace
.active_namespaces
.get(&namespace.prefix.as_deref())
.copied()
.is_some_and(|uri| uri == namespace.uri)
{
continue;
}
output.push_str(" xmlns");
if let Some(prefix) = &namespace.prefix {
output.push(':');
reject_unrepresentable_markup(prefix, encoding, "namespace prefix")?;
output.push_str(prefix);
}
output.push_str("=\"");
escape_attribute(
&namespace.uri,
definition.xml_version,
encoding,
definition.method == OutputMethod::Xml,
output,
)?;
output.push('"');
workspace.bind_namespace(
namespace.prefix.as_deref(),
namespace.uri.as_str(),
meter,
)?;
}
for attribute in attributes {
output.push(' ');
push_name(
attribute.prefix.as_deref(),
&attribute.name.local,
encoding,
output,
)?;
if definition.method == OutputMethod::Html
&& name.namespace.is_none()
&& attribute.name.namespace.is_none()
&& is_html_boolean_attribute(&name.local, &attribute.name.local)
&& attribute.value.eq_ignore_ascii_case(&attribute.name.local)
{
continue;
}
output.push_str("=\"");
let html_uri_escaping = if definition.method == OutputMethod::Html
&& name.namespace.is_none()
&& attribute.name.namespace.is_none()
{
html_uri_escaping(&name.local, &attribute.name.local)
} else {
None
};
if let Some(escaping) = html_uri_escaping {
escape_html_uri_attribute(&attribute.value, escaping, encoding, output);
} else if definition.method == OutputMethod::Html && name.namespace.is_none() {
escape_html_attribute(&attribute.value, encoding, output);
} else {
escape_attribute(
&attribute.value,
definition.xml_version,
encoding,
definition.method == OutputMethod::Xml,
output,
)?;
}
output.push('"');
}
if node.children.is_empty() && definition.method == OutputMethod::Xml {
output.push_str(
if name.namespace.as_deref() == Some("http://www.w3.org/1999/xhtml")
&& definition
.doctype_public
.as_deref()
.is_some_and(|public| public.contains("XHTML"))
{
" />"
} else {
"/>"
},
);
workspace.rollback_namespaces(namespace_rollback_len);
continue;
}
output.push('>');
let html_head = definition.method == OutputMethod::Html
&& name.local.eq_ignore_ascii_case("head")
&& (is_html_output_namespace(name.namespace.as_deref())
|| (name.namespace.as_deref() == Some("http://www.w3.org/1999/xhtml")
&& definition
.doctype_public
.as_deref()
.is_some_and(|public| public.contains("XHTML"))));
if definition.inject_content_type && html_head {
if definition.indent {
output.push('\n');
output.push_repeated(
b' ',
context.depth.saturating_add(1).saturating_mul(2),
)?;
}
output.push_str(
"<meta http-equiv=\"Content-Type\" content=\"text/html; charset=",
);
escape_attribute(
&definition.encoding,
definition.xml_version,
encoding,
definition.method == OutputMethod::Xml,
output,
)?;
output.push_str(if definition.method == OutputMethod::Html {
"\">"
} else {
"\" />"
});
}
let cdata = definition.method == OutputMethod::Xml
&& definition.cdata_section_elements.contains(name);
let html_whitespace_sensitive = context.html_whitespace_sensitive
|| (definition.indent
&& definition.method == OutputMethod::Html
&& name.namespace.is_none()
&& ["pre", "textarea", "script", "style"]
.iter()
.any(|candidate| name.local.eq_ignore_ascii_case(candidate)));
let child_context = RenderContext {
depth: context.depth + 1,
parent: Some(id),
parent_mixed: context.parent_mixed || mixed,
html_whitespace_sensitive,
};
let html_void = definition.method == OutputMethod::Html
&& name.namespace.is_none()
&& [
"area", "base", "basefont", "br", "col", "frame", "hr", "img", "input",
"isindex", "link", "meta", "param",
]
.iter()
.any(|candidate| name.local.eq_ignore_ascii_case(candidate));
workspace.push(
RenderTask::CloseElement {
id,
depth: context.depth,
mixed,
parent_mixed: context.parent_mixed,
html_whitespace_sensitive,
has_element_child: node.children.iter().any(|child| {
matches!(
document.node(*child).map(|node| &node.kind),
Some(NodeKind::Element { .. })
)
}),
html_void,
namespace_rollback_len,
},
meter,
)?;
workspace.push(
RenderTask::Children {
parent: id,
next: 0,
context: child_context,
cdata,
skip_legacy_content_type: definition.inject_content_type && html_head,
},
meter,
)?;
}
_ => {}
}
}
Ok(())
}
fn is_html_output_namespace(namespace: Option<&str>) -> bool {
namespace.is_none()
}
fn push_cdata(value: &str, version: &str, encoding: &OutputEncoding, output: &mut RenderBuffer) {
output.push_str("<![CDATA[");
CdataWriter::new(version, encoding, output).write_and_finish(value);
output.push_str("]]>");
}
fn push_cdata_range(
document: &Document,
parent: NodeId,
start: usize,
end: usize,
version: &str,
encoding: &OutputEncoding,
output: &mut RenderBuffer,
) -> Result<()> {
let children = &document
.node(parent)
.ok_or_else(|| Error::Serialization("CDATA parent disappeared".into()))?
.children;
let children = children
.get(start..end)
.ok_or_else(|| Error::Serialization("CDATA text range is stale".into()))?;
output.push_str("<![CDATA[");
let mut writer = CdataWriter::new(version, encoding, output);
for child in children {
let Some(NodeKind::Text {
value,
disable_output_escaping: false,
}) = document.node(*child).map(|node| &node.kind)
else {
return Err(Error::Serialization(
"CDATA text run changed during serialization".into(),
));
};
writer.write(value);
}
writer.finish();
output.push_str("]]>");
Ok(())
}
struct CdataWriter<'a> {
version: &'a str,
encoding: &'a OutputEncoding,
output: &'a mut RenderBuffer,
trailing_brackets: usize,
}
impl<'a> CdataWriter<'a> {
fn new(version: &'a str, encoding: &'a OutputEncoding, output: &'a mut RenderBuffer) -> Self {
Self {
version,
encoding,
output,
trailing_brackets: 0,
}
}
fn write_and_finish(mut self, value: &str) {
self.write(value);
self.finish();
}
fn write(&mut self, value: &str) {
let mut start = 0usize;
for (offset, character) in value.char_indices() {
let reference = if character == '\r' {
Some(false)
} else if self.version == "1.1" && xml11_requires_reference(character) {
Some(true)
} else if !self.encoding.represents(character) {
Some(false)
} else {
None
};
let Some(hexadecimal) = reference else {
continue;
};
self.push_segment(&value[start..offset]);
self.flush_trailing_brackets();
self.output.push_str("]]>");
if hexadecimal {
push_hex_reference(self.output, character);
} else {
push_decimal_reference(self.output, character);
}
self.output.push_str("<![CDATA[");
start = offset + character.len_utf8();
}
self.push_segment(&value[start..]);
}
fn finish(mut self) {
self.flush_trailing_brackets();
}
fn push_segment(&mut self, value: &str) {
let trailing_brackets = value
.as_bytes()
.iter()
.rev()
.take(2)
.take_while(|byte| **byte == b']')
.count();
let body_end = value.len() - trailing_brackets;
let body = &value[..body_end];
if body.is_empty() {
let total = self.trailing_brackets + trailing_brackets;
self.push_brackets(total.saturating_sub(2));
self.trailing_brackets = total.min(2);
return;
}
let leading_brackets = body
.as_bytes()
.iter()
.take_while(|byte| **byte == b']')
.count();
if body.as_bytes().get(leading_brackets) == Some(&b'>')
&& self.trailing_brackets + leading_brackets >= 2
{
let excess = self.trailing_brackets + leading_brackets - 2;
let pending_excess = excess.min(self.trailing_brackets);
self.push_brackets(pending_excess);
let body_excess = excess - pending_excess;
self.output.push_str(&body[..body_excess]);
self.output.push_str("]]]]><![CDATA[>");
push_cdata_segment(&body[leading_brackets + 1..], self.output);
} else {
self.flush_trailing_brackets();
push_cdata_segment(body, self.output);
}
self.trailing_brackets = trailing_brackets;
}
fn flush_trailing_brackets(&mut self) {
self.push_brackets(self.trailing_brackets);
self.trailing_brackets = 0;
}
fn push_brackets(&mut self, count: usize) {
for _ in 0..count {
self.output.push(']');
}
}
}
fn push_cdata_segment(value: &str, output: &mut RenderBuffer) {
let mut fragments = value.split("]]>");
if let Some(first) = fragments.next() {
output.push_str(first);
}
for fragment in fragments {
output.push_str("]]]]><![CDATA[>");
output.push_str(fragment);
}
}
fn push_xml_raw_text(
value: &str,
version: &str,
encoding: &OutputEncoding,
output: &mut RenderBuffer,
) {
for character in value.chars() {
if version == "1.1" && xml11_requires_reference(character) {
push_hex_reference(output, character);
} else if !encoding.represents(character) {
push_decimal_reference(output, character);
} else {
output.push(character);
}
}
}
fn push_hex_reference(output: &mut RenderBuffer, character: char) {
write!(output, "&#x{:X};", u32::from(character))
.expect("writing character reference to render buffer cannot fail");
}
fn push_decimal_reference(output: &mut RenderBuffer, character: char) {
write!(output, "&#{};", u32::from(character))
.expect("writing character reference to render buffer cannot fail");
}
fn reject_xml11_restricted_markup(
value: &str,
definition: &EffectiveOutputDefinition<'_>,
kind: &str,
) -> Result<()> {
if definition.method == OutputMethod::Xml
&& definition.xml_version == "1.1"
&& value.chars().any(is_xml11_restricted)
{
return Err(Error::Serialization(format!(
"XML 1.1 {kind} cannot contain restricted control characters"
)));
}
Ok(())
}
#[derive(Clone, Copy)]
enum HtmlUriEscaping {
NonAsciiAndSpaces,
NonAscii,
}
fn html_uri_escaping(element: &str, attribute: &str) -> Option<HtmlUriEscaping> {
let libxslt_pair = (attribute.eq_ignore_ascii_case("action")
&& element.eq_ignore_ascii_case("form"))
|| (attribute.eq_ignore_ascii_case("cite")
&& ascii_eq_any(element, &["blockquote", "q", "del", "ins"]))
|| (attribute.eq_ignore_ascii_case("href")
&& ascii_eq_any(element, &["a", "area", "link", "base"]))
|| (attribute.eq_ignore_ascii_case("longdesc")
&& ascii_eq_any(element, &["img", "frame", "iframe"]))
|| (attribute.eq_ignore_ascii_case("src")
&& ascii_eq_any(element, &["img", "input", "frame", "iframe", "script"]))
|| (attribute.eq_ignore_ascii_case("usemap")
&& ascii_eq_any(element, &["img", "input", "object"]));
if libxslt_pair {
return Some(HtmlUriEscaping::NonAsciiAndSpaces);
}
((attribute.eq_ignore_ascii_case("background") && element.eq_ignore_ascii_case("body"))
|| (attribute.eq_ignore_ascii_case("profile") && element.eq_ignore_ascii_case("head"))
|| (ascii_eq_any(attribute, &["classid", "data"])
&& element.eq_ignore_ascii_case("object"))
|| (ascii_eq_any(attribute, &["archive", "codebase"])
&& ascii_eq_any(element, &["applet", "object"])))
.then_some(HtmlUriEscaping::NonAscii)
}
fn escape_html_uri_attribute(
value: &str,
escaping: HtmlUriEscaping,
encoding: &OutputEncoding,
output: &mut RenderBuffer,
) {
let escape_spaces = matches!(escaping, HtmlUriEscaping::NonAsciiAndSpaces);
let mut plain_start = 0;
for (index, character) in value.char_indices() {
if !character.is_ascii() || (escape_spaces && character == ' ') {
escape_html_attribute(&value[plain_start..index], encoding, output);
let mut utf8 = [0_u8; 4];
for byte in character.encode_utf8(&mut utf8).bytes() {
const HEX: &[u8; 16] = b"0123456789ABCDEF";
output.push('%');
output.push(char::from(HEX[usize::from(byte >> 4)]));
output.push(char::from(HEX[usize::from(byte & 0x0f)]));
}
plain_start = index + character.len_utf8();
}
}
escape_html_attribute(&value[plain_start..], encoding, output);
}
fn is_replaceable_legacy_content_type_meta(node: &super::Node) -> bool {
let NodeKind::Element {
name, attributes, ..
} = &node.kind
else {
return false;
};
name.namespace.is_none()
&& name.local.eq_ignore_ascii_case("meta")
&& attributes.iter().any(|attribute| {
attribute.name.namespace.is_none()
&& attribute.name.local.eq_ignore_ascii_case("http-equiv")
&& attribute.value.eq_ignore_ascii_case("content-type")
})
}
fn push_name(
prefix: Option<&str>,
local: &str,
encoding: &OutputEncoding,
output: &mut RenderBuffer,
) -> Result<()> {
if let Some(prefix) = prefix {
reject_unrepresentable_markup(prefix, encoding, "XML name")?;
output.push_str(prefix);
output.push(':');
}
reject_unrepresentable_markup(local, encoding, "XML name")?;
output.push_str(local);
Ok(())
}
fn escape_text(
value: &str,
version: &str,
encoding: &OutputEncoding,
validate_xml: bool,
output: &mut RenderBuffer,
) -> Result<()> {
for character in value.chars() {
if validate_xml {
validate_xml_character(character, version)?;
}
if version == "1.1" && xml11_requires_reference(character) {
push_hex_reference(output, character);
continue;
} else if !encoding.represents(character) {
push_decimal_reference(output, character);
continue;
}
match character {
'&' => output.push_str("&"),
'<' => output.push_str("<"),
'>' => output.push_str(">"),
'\r' => output.push_str(" "),
_ => output.push(character),
}
}
Ok(())
}
fn escape_attribute(
value: &str,
version: &str,
encoding: &OutputEncoding,
validate_xml: bool,
output: &mut RenderBuffer,
) -> Result<()> {
for character in value.chars() {
if validate_xml {
validate_xml_character(character, version)?;
}
if version == "1.1" && xml11_requires_reference(character) {
push_hex_reference(output, character);
continue;
} else if !encoding.represents(character) {
push_decimal_reference(output, character);
continue;
}
match character {
'&' => output.push_str("&"),
'<' => output.push_str("<"),
'"' => output.push_str("""),
'\r' => output.push_str(" "),
'\n' => output.push_str(" "),
'\t' => output.push_str("	"),
_ => output.push(character),
}
}
Ok(())
}
fn is_xml11_restricted(character: char) -> bool {
matches!(
u32::from(character),
0x1..=0x8 | 0xB..=0xC | 0xE..=0x1F | 0x7F..=0x84 | 0x86..=0x9F
)
}
fn xml11_requires_reference(character: char) -> bool {
is_xml11_restricted(character) || matches!(character, '\u{85}' | '\u{2028}')
}
fn is_html_boolean_attribute(element: &str, attribute: &str) -> bool {
(attribute.eq_ignore_ascii_case("nohref") && element.eq_ignore_ascii_case("area"))
|| (attribute.eq_ignore_ascii_case("compact")
&& ascii_eq_any(element, &["dir", "menu", "ol", "ul", "dl"]))
|| (attribute.eq_ignore_ascii_case("noresize") && element.eq_ignore_ascii_case("frame"))
|| (attribute.eq_ignore_ascii_case("noshade") && element.eq_ignore_ascii_case("hr"))
|| (attribute.eq_ignore_ascii_case("ismap") && ascii_eq_any(element, &["img", "input"]))
|| (attribute.eq_ignore_ascii_case("checked") && element.eq_ignore_ascii_case("input"))
|| (attribute.eq_ignore_ascii_case("declare") && element.eq_ignore_ascii_case("object"))
|| (attribute.eq_ignore_ascii_case("selected") && element.eq_ignore_ascii_case("option"))
|| (attribute.eq_ignore_ascii_case("defer") && element.eq_ignore_ascii_case("script"))
|| (attribute.eq_ignore_ascii_case("multiple") && element.eq_ignore_ascii_case("select"))
|| (attribute.eq_ignore_ascii_case("nowrap") && ascii_eq_any(element, &["td", "th"]))
|| (attribute.eq_ignore_ascii_case("readonly")
&& ascii_eq_any(element, &["input", "textarea"]))
|| (attribute.eq_ignore_ascii_case("disabled")
&& ascii_eq_any(
element,
&[
"button", "input", "optgroup", "option", "select", "textarea",
],
))
}
fn ascii_eq_any(value: &str, candidates: &[&str]) -> bool {
candidates
.iter()
.any(|candidate| value.eq_ignore_ascii_case(candidate))
}
fn escape_html_attribute(value: &str, encoding: &OutputEncoding, output: &mut RenderBuffer) {
for character in value.chars() {
if !encoding.represents(character) {
push_decimal_reference(output, character);
continue;
}
match character {
'&' => output.push_str("&"),
'"' => output.push_str("""),
_ => output.push(character),
}
}
}
fn reject_unrepresentable_markup(value: &str, encoding: &OutputEncoding, kind: &str) -> Result<()> {
if let Some(character) = value
.chars()
.find(|character| !encoding.represents(*character))
{
return Err(Error::Serialization(format!(
"{kind} character `{character}` is not representable in the output encoding"
)));
}
Ok(())
}
fn encode(
value: String,
utf8_bytes: usize,
encoded_bytes: usize,
encoding: &OutputEncoding,
meter: &mut Meter,
budget_kind: BudgetKind,
) -> Result<Vec<u8>> {
meter.charge(budget_kind, encoded_bytes)?;
if matches!(encoding, OutputEncoding::Utf8 | OutputEncoding::Ascii) {
if matches!(encoding, OutputEncoding::Ascii) && !value.is_ascii() {
return Err(Error::Serialization(
"non-ASCII character reached the US-ASCII encoder without escaping".into(),
));
}
return Ok(value.into_bytes());
}
meter.charge(BudgetKind::OwnedBytes, encoded_bytes)?;
if let OutputEncoding::Registered(encoding) = encoding {
let mut bytes = Vec::with_capacity(encoded_bytes);
for character in value.chars() {
let byte = encoding.encode_char(character).ok_or_else(|| {
Error::Serialization(format!(
"character `{character}` reached the {} encoder without escaping",
encoding.name()
))
})?;
bytes.push(byte);
}
meter.release_owned_bytes(utf8_bytes);
return Ok(bytes);
}
if matches!(encoding, OutputEncoding::Utf16 | OutputEncoding::Utf16Le) {
let mut bytes = Vec::with_capacity(encoded_bytes);
if matches!(encoding, OutputEncoding::Utf16) {
bytes.extend_from_slice(&[0xFF, 0xFE]);
}
for unit in value.encode_utf16() {
bytes.extend_from_slice(&unit.to_le_bytes());
}
meter.release_owned_bytes(utf8_bytes);
return Ok(bytes);
}
if matches!(encoding, OutputEncoding::Utf16Be) {
let mut bytes = Vec::with_capacity(encoded_bytes);
for unit in value.encode_utf16() {
bytes.extend_from_slice(&unit.to_be_bytes());
}
meter.release_owned_bytes(utf8_bytes);
return Ok(bytes);
}
let OutputEncoding::Other { encoding, .. } = encoding else {
unreachable!("all built-in output encodings returned above")
};
let mut encoder = encoding.new_encoder();
let mut source = value.as_str();
let mut bytes = Vec::with_capacity(encoded_bytes);
let mut buffer = [0_u8; 4096];
loop {
let (result, read, written, _) = encoder.encode_from_utf8(source, &mut buffer, true);
bytes.extend_from_slice(&buffer[..written]);
source = &source[read..];
match result {
encoding_rs::CoderResult::InputEmpty => break,
encoding_rs::CoderResult::OutputFull => {}
}
}
meter.release_owned_bytes(utf8_bytes);
Ok(bytes)
}
fn validate_text_encoding(value: &str, encoding: &OutputEncoding, label: &str) -> Result<()> {
if let Some(character) = value
.chars()
.find(|character| !encoding.represents(*character))
{
return Err(Error::Serialization(format!(
"text output character `{character}` is not representable in {label}"
)));
}
Ok(())
}
fn infer_html_output(document: &Document) -> bool {
let Some(root) = document.node(document.root()) else {
return false;
};
for child in &root.children {
match document.node(*child).map(|node| &node.kind) {
Some(NodeKind::Text { value, .. })
if !value.chars().all(super::lexical::is_xml_whitespace) =>
{
return false;
}
Some(NodeKind::Element { name, .. }) => {
return name.namespace.is_none() && name.local.eq_ignore_ascii_case("html");
}
_ => {}
}
}
false
}
fn validate_document_output_shape(document: &Document) -> Result<()> {
let root = document
.node(document.root())
.ok_or_else(|| Error::Serialization("missing result root".into()))?;
let mut has_element = false;
for child in &root.children {
match document.node(*child).map(|node| &node.kind) {
Some(NodeKind::Element { .. }) if !has_element => has_element = true,
Some(NodeKind::Comment(_) | NodeKind::ProcessingInstruction { .. }) => {},
Some(NodeKind::Text { value, .. }) if value.chars().all(super::lexical::is_xml_whitespace) => {},
_ => return Err(Error::Serialization("standalone/DOCTYPE output requires one document element and no top-level character data".into())),
}
}
if !has_element {
return Err(Error::Serialization(
"standalone/DOCTYPE output requires a document element".into(),
));
}
Ok(())
}
fn validate_xml_characters(value: &str, version: &str) -> Result<()> {
for character in value.chars() {
validate_xml_character(character, version)?;
}
Ok(())
}
fn validate_xml_character(character: char, version: &str) -> Result<()> {
let code = u32::from(character);
let valid = if version == "1.1" {
matches!(code, 0x1..=0xD7FF | 0xE000..=0xFFFD | 0x10000..=0x10FFFF)
} else {
super::lexical::is_xml10_character(character)
};
if !valid {
return Err(Error::Serialization(format!(
"character U+{code:04X} is forbidden by XML {version}"
)));
}
Ok(())
}
#[cfg(all(test, not(feature = "embedded")))]
mod tests;