#[inline]
pub(crate) fn is_xml_whitespace_only(text: &str) -> bool {
text.chars()
.all(|ch| matches!(ch, ' ' | '\t' | '\r' | '\n'))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct XmlBase64NormalizeError {
pub invalid_byte: u8,
pub normalized_offset: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct XmlBase64LengthError {
pub max_len: usize,
}
pub(crate) fn normalize_xml_base64_text(
text: &str,
normalized: &mut String,
) -> Result<(), XmlBase64NormalizeError> {
normalize_xml_base64_text_with_limit(text, normalized, usize::MAX).map_err(|err| match err {
XmlBase64NormalizeLimitedError::InvalidWhitespace(err) => err,
XmlBase64NormalizeLimitedError::TooLong(_) => unreachable!("unbounded normalization"),
})
}
pub(crate) fn normalize_xml_base64_text_with_limit(
text: &str,
normalized: &mut String,
max_len: usize,
) -> Result<(), XmlBase64NormalizeLimitedError> {
for ch in text.chars() {
if matches!(ch, ' ' | '\t' | '\r' | '\n') {
continue;
}
if ch.is_ascii_whitespace() {
let mut utf8 = [0_u8; 4];
let encoded = ch.encode_utf8(&mut utf8);
let invalid_byte = encoded.as_bytes()[0];
return Err(XmlBase64NormalizeLimitedError::InvalidWhitespace(
XmlBase64NormalizeError {
invalid_byte,
normalized_offset: normalized.len(),
},
));
}
if normalized.len() + ch.len_utf8() > max_len {
return Err(XmlBase64NormalizeLimitedError::TooLong(
XmlBase64LengthError { max_len },
));
}
normalized.push(ch);
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum XmlBase64NormalizeLimitedError {
InvalidWhitespace(XmlBase64NormalizeError),
TooLong(XmlBase64LengthError),
}
impl From<XmlBase64NormalizeError> for XmlBase64NormalizeLimitedError {
fn from(err: XmlBase64NormalizeError) -> Self {
Self::InvalidWhitespace(err)
}
}
#[cfg(test)]
mod tests {
use super::{
XmlBase64NormalizeLimitedError, normalize_xml_base64_text,
normalize_xml_base64_text_with_limit,
};
#[test]
fn bounded_base64_normalization_rejects_before_growth() {
let mut normalized = String::from("ABCD");
let err = normalize_xml_base64_text_with_limit(" E", &mut normalized, 4)
.expect_err("bounded normalization must reject before appending past the cap");
assert!(matches!(err, XmlBase64NormalizeLimitedError::TooLong(_)));
assert_eq!(normalized, "ABCD");
}
#[test]
fn unbounded_base64_normalization_preserves_existing_behavior() {
let mut normalized = String::new();
normalize_xml_base64_text(" A\tB\r\nC ", &mut normalized)
.expect("XML whitespace must be stripped from base64 text");
assert_eq!(normalized, "ABC");
}
}