Skip to main content

html_escape/decode/html_entity/
mod.rs

1mod tables;
2
3use alloc::{borrow::Cow, string::String, vec::Vec};
4use core::str::from_utf8_unchecked;
5#[cfg(feature = "std")]
6use std::io::{self, Write};
7
8pub use tables::*;
9
10use crate::functions::*;
11
12#[derive(Clone, Copy)]
13enum DecodedEntityValue {
14    Named(&'static str),
15    Character(char),
16}
17
18#[derive(Clone, Copy)]
19struct DecodedEntity {
20    start: usize,
21    end:   usize,
22    value: DecodedEntityValue,
23}
24
25#[inline]
26fn decode_named_entity(name: &[u8]) -> Option<DecodedEntityValue> {
27    NAMED_ENTITIES
28        .binary_search_by(|(t_name, _)| t_name.cmp(&name))
29        .ok()
30        .map(|index| DecodedEntityValue::Named(NAMED_ENTITIES[index].1))
31}
32
33/// Turn the value of a numeric character reference into a character, rejecting the code points which cannot appear in an HTML document.
34#[inline]
35fn decode_character(number: u32) -> Option<char> {
36    let character = char::try_from(number).ok()?;
37
38    match character {
39        // These are the ASCII whitespace characters, which are legitimate in an HTML document.
40        '\t' | '\n' | '\u{000C}' | '\r' => Some(character),
41        // U+0000 and the other C0 control characters are not.
42        '\0'..='\u{001F}' => None,
43        _ => Some(character),
44    }
45}
46
47#[inline]
48fn decode_decimal_entity(text: &str, start: usize, end: usize) -> Option<DecodedEntityValue> {
49    let digits = &text[start..end];
50
51    // `parse` accepts a leading sign, but a numeric character reference consists of digits only.
52    if !digits.as_bytes().first()?.is_ascii_digit() {
53        return None;
54    }
55
56    let character = decode_character(digits.parse::<u32>().ok()?)?;
57
58    Some(DecodedEntityValue::Character(character))
59}
60
61#[inline]
62fn decode_hex_entity(text: &str, start: usize, end: usize) -> Option<DecodedEntityValue> {
63    let digits = &text[start..end];
64
65    // `from_str_radix` accepts a leading sign, but a numeric character reference consists of digits only.
66    if !digits.as_bytes().first()?.is_ascii_hexdigit() {
67        return None;
68    }
69
70    let character = decode_character(u32::from_str_radix(digits, 16).ok()?)?;
71
72    Some(DecodedEntityValue::Character(character))
73}
74
75fn find_decoded_entity(text: &str, start: usize) -> Option<DecodedEntity> {
76    let text_bytes = text.as_bytes();
77    let text_length = text_bytes.len();
78
79    let mut p = start;
80
81    'search: while p < text_length {
82        if text_bytes[p] != b'&' {
83            p += 1;
84
85            continue;
86        }
87
88        let entity_start = p;
89
90        p += 1;
91
92        if p == text_length {
93            return None;
94        }
95
96        match text_bytes[p] {
97            b'&' => continue 'search,
98            b';' => {
99                p += 1;
100            },
101            b'#' => {
102                p += 1;
103
104                if p == text_length {
105                    return None;
106                }
107
108                match text_bytes[p] {
109                    b'&' => continue 'search,
110                    b';' => {
111                        p += 1;
112                    },
113                    b'x' | b'X' => {
114                        p += 1;
115
116                        if p == text_length {
117                            return None;
118                        }
119
120                        match text_bytes[p] {
121                            b'&' => continue 'search,
122                            b';' => {
123                                p += 1;
124                            },
125                            _ => {
126                                let hex_start = p;
127
128                                loop {
129                                    p += 1;
130
131                                    if p == text_length {
132                                        return None;
133                                    }
134
135                                    match text_bytes[p] {
136                                        b'&' => continue 'search,
137                                        b';' => {
138                                            if let Some(value) =
139                                                decode_hex_entity(text, hex_start, p)
140                                            {
141                                                return Some(DecodedEntity {
142                                                    start: entity_start,
143                                                    end: p + 1,
144                                                    value,
145                                                });
146                                            }
147
148                                            p += 1;
149
150                                            continue 'search;
151                                        },
152                                        _ => (),
153                                    }
154                                }
155                            },
156                        }
157                    },
158                    _ => {
159                        let number_start = p;
160
161                        loop {
162                            p += 1;
163
164                            if p == text_length {
165                                return None;
166                            }
167
168                            match text_bytes[p] {
169                                b'&' => continue 'search,
170                                b';' => {
171                                    if let Some(value) =
172                                        decode_decimal_entity(text, number_start, p)
173                                    {
174                                        return Some(DecodedEntity {
175                                            start: entity_start,
176                                            end: p + 1,
177                                            value,
178                                        });
179                                    }
180
181                                    p += 1;
182
183                                    continue 'search;
184                                },
185                                _ => (),
186                            }
187                        }
188                    },
189                }
190            },
191            _ => {
192                let name_start = p;
193
194                loop {
195                    p += 1;
196
197                    if p == text_length {
198                        return None;
199                    }
200
201                    match text_bytes[p] {
202                        b'&' => continue 'search,
203                        b';' => {
204                            if let Some(value) = decode_named_entity(&text_bytes[name_start..p]) {
205                                return Some(DecodedEntity {
206                                    start: entity_start,
207                                    end: p + 1,
208                                    value,
209                                });
210                            }
211
212                            p += 1;
213
214                            continue 'search;
215                        },
216                        _ => (),
217                    }
218                }
219            },
220        }
221    }
222
223    None
224}
225
226#[inline]
227fn write_decoded_entity_to_vec(value: DecodedEntityValue, output: &mut Vec<u8>) {
228    match value {
229        DecodedEntityValue::Named(entity) => output.extend_from_slice(entity.as_bytes()),
230        DecodedEntityValue::Character(character) => write_char_to_vec(character, output),
231    }
232}
233
234#[cfg(feature = "std")]
235#[inline]
236fn write_decoded_entity_to_writer<W: Write>(
237    value: DecodedEntityValue,
238    output: &mut W,
239) -> Result<(), io::Error> {
240    match value {
241        DecodedEntityValue::Named(entity) => output.write_all(entity.as_bytes()),
242        DecodedEntityValue::Character(character) => write_char_to_writer(character, output),
243    }
244}
245
246/// Decode HTML entities in a given string.
247///
248/// Only exact named and numeric character references ending with `;` are decoded.
249/// Invalid Unicode scalar values, null, and C0 controls other than ASCII whitespace are left unchanged.
250/// This function does not implement legacy semicolonless references or C1 control replacements.
251pub fn decode_html_entities<S: ?Sized + AsRef<str>>(text: &S) -> Cow<'_, str> {
252    let text = text.as_ref();
253    let text_bytes = text.as_bytes();
254    let text_length = text_bytes.len();
255
256    let entity = match find_decoded_entity(text, 0) {
257        Some(entity) => entity,
258        None => return Cow::from(text),
259    };
260
261    let mut v = Vec::with_capacity(text_length);
262
263    v.extend_from_slice(&text_bytes[..entity.start]);
264    write_decoded_entity_to_vec(entity.value, &mut v);
265
266    // `entity.end` is right after a `;`, so it is a UTF-8 boundary.
267    decode_html_entities_to_vec(&text[entity.end..], &mut v);
268
269    // SAFETY: every decoded entity is a `char` or a `&str`, and every copied slice is bounded by ASCII bytes, so `v` is valid UTF-8.
270    Cow::from(unsafe { String::from_utf8_unchecked(v) })
271}
272
273/// Decode HTML entities in a given string to a mutable `String` reference and return the decoded string slice.
274///
275/// Only exact named and numeric character references ending with `;` are decoded.
276/// Invalid Unicode scalar values, null, and C0 controls other than ASCII whitespace are left unchanged.
277/// This function does not implement legacy semicolonless references or C1 control replacements.
278pub fn decode_html_entities_to_string<S: AsRef<str>>(text: S, output: &mut String) -> &str {
279    // SAFETY: the decoded data is valid UTF-8, so `output` remains a valid `String`.
280    unsafe { from_utf8_unchecked(decode_html_entities_to_vec(text, output.as_mut_vec())) }
281}
282
283/// Decode HTML entities in a given string to a mutable `Vec<u8>` reference and return the decoded data slice.
284///
285/// Only exact named and numeric character references ending with `;` are decoded.
286/// Invalid Unicode scalar values, null, and C0 controls other than ASCII whitespace are left unchanged.
287/// This function does not implement legacy semicolonless references or C1 control replacements.
288pub fn decode_html_entities_to_vec<S: AsRef<str>>(text: S, output: &mut Vec<u8>) -> &[u8] {
289    let text = text.as_ref();
290    let text_bytes = text.as_bytes();
291    let text_length = text_bytes.len();
292
293    output.reserve(text_length);
294
295    let current_length = output.len();
296
297    let mut start = 0;
298
299    while let Some(entity) = find_decoded_entity(text, start) {
300        output.extend_from_slice(&text_bytes[start..entity.start]);
301        write_decoded_entity_to_vec(entity.value, output);
302        start = entity.end;
303    }
304
305    output.extend_from_slice(&text_bytes[start..]);
306
307    &output[current_length..]
308}
309
310#[cfg(feature = "std")]
311/// Decode HTML entities in a given string to a writer.
312///
313/// Only exact named and numeric character references ending with `;` are decoded.
314/// Invalid Unicode scalar values, null, and C0 controls other than ASCII whitespace are left unchanged.
315/// This function does not implement legacy semicolonless references or C1 control replacements.
316pub fn decode_html_entities_to_writer<S: AsRef<str>, W: Write>(
317    text: S,
318    output: &mut W,
319) -> Result<(), io::Error> {
320    let text = text.as_ref();
321    let text_bytes = text.as_bytes();
322
323    let mut start = 0;
324
325    while let Some(entity) = find_decoded_entity(text, start) {
326        output.write_all(&text_bytes[start..entity.start])?;
327        write_decoded_entity_to_writer(entity.value, output)?;
328        start = entity.end;
329    }
330
331    output.write_all(&text_bytes[start..])
332}