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