use crate::{Transform, TransformError, TransformerCategory};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HtmlDecode;
pub const DEFAULT_TEST_INPUT: &str = "<p>Hello & Welcome!</p>";
impl Transform for HtmlDecode {
fn name(&self) -> &'static str {
"HTML Decode"
}
fn id(&self) -> &'static str {
"htmldecode"
}
fn description(&self) -> &'static str {
"Decodes HTML entities (e.g., <) back into characters (<)."
}
fn category(&self) -> TransformerCategory {
TransformerCategory::Decoder
}
fn default_test_input(&self) -> &'static str {
"<p>Hello & Welcome!</p>"
}
fn transform(&self, input: &str) -> Result<String, TransformError> {
if input.is_empty() {
return Ok(String::new());
}
let mut result = String::with_capacity(input.len());
let mut chars = input.chars().peekable();
while let Some(c) = chars.next() {
if c == '&' {
let mut entity = String::with_capacity(10); entity.push(c);
let mut entity_length = 1; const MAX_ENTITY_LENGTH: usize = 12;
while let Some(&next_char) = chars.peek() {
if next_char == ';' || entity_length >= MAX_ENTITY_LENGTH {
entity.push(next_char);
chars.next(); break;
}
entity.push(next_char);
chars.next(); entity_length += 1;
}
if let Some(decoded) = decode_html_entity(&entity) {
result.push(decoded);
} else {
result.push_str(&entity);
}
} else {
result.push(c);
}
}
Ok(result)
}
}
fn decode_html_entity(entity: &str) -> Option<char> {
match entity {
"&" => Some('&'),
"<" => Some('<'),
">" => Some('>'),
""" => Some('"'),
"'" => Some('\''),
"/" => Some('/'),
"`" => Some('`'),
"=" => Some('='),
_ if entity.starts_with("&#x") && entity.ends_with(';') => {
let hex_str = &entity[3..entity.len() - 1];
u32::from_str_radix(hex_str, 16)
.ok()
.and_then(std::char::from_u32)
}
_ if entity.starts_with("&#") && entity.ends_with(';') => {
let num_str = &entity[2..entity.len() - 1];
num_str.parse::<u32>().ok().and_then(std::char::from_u32)
}
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_html_decode() {
let decoder = HtmlDecode;
assert_eq!(
decoder.transform(DEFAULT_TEST_INPUT).unwrap(),
"<p>Hello & Welcome!</p>"
);
assert_eq!(
decoder
.transform("<script>alert("XSS attack");</script>")
.unwrap(),
"<script>alert(\"XSS attack\");</script>"
);
assert_eq!(
decoder.transform("a < b && c > d").unwrap(),
"a < b && c > d"
);
assert_eq!(
decoder
.transform("Don't use `eval(input)` or query='unsafe'")
.unwrap(),
"Don't use `eval(input)` or query='unsafe'"
);
assert_eq!(
decoder
.transform("Euro symbol: € or €")
.unwrap(),
"Euro symbol: € or €"
);
assert_eq!(
decoder.transform("Normal text with no entities").unwrap(),
"Normal text with no entities"
);
assert_eq!(decoder.transform("").unwrap(), "");
assert_eq!(
decoder.transform("This is an &incomplete entity").unwrap(),
"This is an &incomplete entity"
);
assert_eq!(
decoder
.transform("This is &invalid; and &#invalid;")
.unwrap(),
"This is &invalid; and &#invalid;"
);
}
}