#[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_bytes(
bytes: &[u8],
normalized: &mut Vec<u8>,
accept: impl Fn(u8) -> bool,
) -> Result<(), XmlBase64NormalizeError> {
visit_xml_base64_bytes(bytes, normalized.len(), &accept, |byte| {
normalized.push(byte);
})
}
fn visit_xml_base64_bytes(
bytes: &[u8],
initial_offset: usize,
accept: &impl Fn(u8) -> bool,
mut visit: impl FnMut(u8),
) -> Result<(), XmlBase64NormalizeError> {
let mut accepted = 0_usize;
for &byte in bytes {
if matches!(byte, b' ' | b'\t' | b'\r' | b'\n') {
continue;
}
if byte.is_ascii_whitespace() || !accept(byte) {
return Err(XmlBase64NormalizeError {
invalid_byte: byte,
normalized_offset: initial_offset + accepted,
});
}
visit(byte);
accepted += 1;
}
Ok(())
}
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_bytes, 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");
}
#[test]
fn byte_normalization_applies_caller_alphabet_policy() {
let mut normalized = Vec::new();
let err = normalize_xml_base64_bytes(b" A\tB!", &mut normalized, |byte| {
byte.is_ascii_alphanumeric()
})
.expect_err("caller must be able to reject a non-alphabet byte");
assert_eq!(normalized, b"AB");
assert_eq!(err.invalid_byte, b'!');
assert_eq!(err.normalized_offset, 2);
}
}