rama_http/protocols/html/rewrite/
element.rs1use std::borrow::Cow;
5use std::fmt;
6use std::str::FromStr;
7
8use rama_core::error::BoxError;
9use rama_utils::byte_set::{set_each, set_range};
10
11use super::super::tokenizer::{HtmlTag, StartTag};
12use super::super::{IntoHtml, escape_attr_value_into};
13
14pub type HandlerResult = Result<(), BoxError>;
16
17const FORBIDDEN_NAME_BYTE: [bool; 256] = set_each(
21 set_each(set_range([false; 256], 0, 0x20), &[0x7f]),
22 b" \"'<>/=",
23);
24
25#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct AttributeName(Cow<'static, str>);
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36#[non_exhaustive]
37pub enum InvalidAttributeName {
38 Empty,
40 ForbiddenByte(u8),
42}
43
44impl fmt::Display for InvalidAttributeName {
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 match self {
47 Self::Empty => f.write_str("empty attribute name"),
48 Self::ForbiddenByte(b) => write!(f, "forbidden byte {b:#04x} in attribute name"),
49 }
50 }
51}
52
53impl std::error::Error for InvalidAttributeName {}
54
55impl AttributeName {
56 #[must_use]
64 pub const fn from_static(name: &'static str) -> Self {
65 assert!(
66 is_valid_name(name.as_bytes()),
67 "invalid HTML attribute name"
68 );
69 Self(Cow::Borrowed(name))
70 }
71
72 #[must_use]
74 pub fn as_str(&self) -> &str {
75 &self.0
76 }
77
78 fn into_name_bytes(self) -> Cow<'static, [u8]> {
79 match self.0 {
80 Cow::Borrowed(s) => Cow::Borrowed(s.as_bytes()),
81 Cow::Owned(s) => Cow::Owned(s.into_bytes()),
82 }
83 }
84}
85
86impl TryFrom<&str> for AttributeName {
87 type Error = InvalidAttributeName;
88
89 fn try_from(name: &str) -> Result<Self, Self::Error> {
90 validate_name(name.as_bytes())?;
91 Ok(Self(Cow::Owned(name.to_owned())))
92 }
93}
94
95impl FromStr for AttributeName {
96 type Err = InvalidAttributeName;
97
98 fn from_str(s: &str) -> Result<Self, Self::Err> {
99 Self::try_from(s)
100 }
101}
102
103const fn is_valid_name(bytes: &[u8]) -> bool {
104 if bytes.is_empty() {
105 return false;
106 }
107 let mut i = 0;
108 while i < bytes.len() {
109 if FORBIDDEN_NAME_BYTE[bytes[i] as usize] {
110 return false;
111 }
112 i += 1;
113 }
114 true
115}
116
117fn validate_name(bytes: &[u8]) -> Result<(), InvalidAttributeName> {
118 if bytes.is_empty() {
119 return Err(InvalidAttributeName::Empty);
120 }
121 match bytes
122 .iter()
123 .copied()
124 .find(|&b| FORBIDDEN_NAME_BYTE[b as usize])
125 {
126 Some(b) => Err(InvalidAttributeName::ForbiddenByte(b)),
127 None => Ok(()),
128 }
129}
130
131pub trait ElementContentHandler {
141 fn handle_element(&mut self, selector: usize, element: &mut Element<'_>) -> HandlerResult;
149}
150
151#[derive(Debug)]
154struct EditedAttribute<'t> {
155 name: Cow<'t, [u8]>,
156 value: Option<Cow<'t, [u8]>>,
158}
159
160#[derive(Debug, Default)]
165enum ElementMode {
166 #[default]
169 Normal,
170 Inner(String),
173 Replace(String),
175 Remove,
177 RemoveKeepContent,
179}
180
181pub struct Element<'t> {
198 tag: &'t StartTag<'t>,
199 before: String,
200 prepend: String,
201 append: String,
202 after: String,
203 attributes: Option<Vec<EditedAttribute<'t>>>,
206 mode: ElementMode,
207}
208
209impl<'t> Element<'t> {
210 pub(crate) fn new(tag: &'t StartTag<'t>) -> Self {
211 Self {
212 tag,
213 before: String::new(),
214 prepend: String::new(),
215 append: String::new(),
216 after: String::new(),
217 attributes: None,
218 mode: ElementMode::Normal,
219 }
220 }
221
222 #[must_use]
225 pub fn tag(&self) -> HtmlTag<'_> {
226 self.tag.tag()
227 }
228
229 #[must_use]
232 pub fn attribute(&self, name: &str) -> Option<&[u8]> {
233 let name = name.as_bytes();
234 match &self.attributes {
235 Some(edited) => edited
236 .iter()
237 .find(|a| a.name.eq_ignore_ascii_case(name))
238 .map(|a| a.value.as_deref().unwrap_or(b"")),
239 None => self
240 .tag
241 .attributes()
242 .find(|a| a.name().eq_ignore_ascii_case(name))
243 .map(|a| if a.has_value() { a.value() } else { b"" }),
244 }
245 }
246
247 #[must_use]
249 pub fn has_attribute(&self, name: &str) -> bool {
250 self.attribute(name).is_some()
251 }
252
253 pub fn set_attribute(&mut self, name: AttributeName, value: &str) {
256 self.ensure_attributes();
257 let Some(attributes) = self.attributes.as_mut() else {
258 return;
259 };
260 let name = name.into_name_bytes();
261 if let Some(existing) = attributes
262 .iter_mut()
263 .find(|a| a.name.eq_ignore_ascii_case(&name))
264 {
265 existing.value = Some(Cow::Owned(value.as_bytes().to_vec()));
266 } else {
267 attributes.push(EditedAttribute {
268 name,
269 value: Some(Cow::Owned(value.as_bytes().to_vec())),
270 });
271 }
272 }
273
274 pub fn remove_attribute(&mut self, name: &str) {
276 self.ensure_attributes();
277 let Some(attributes) = self.attributes.as_mut() else {
278 return;
279 };
280 let name = name.as_bytes();
281 attributes.retain(|a| !a.name.eq_ignore_ascii_case(name));
282 }
283
284 pub fn before(&mut self, content: impl IntoHtml) {
290 reserve_html(&mut self.before, &content);
291 content.escape_and_write(&mut self.before);
292 }
293
294 pub fn prepend(&mut self, content: impl IntoHtml) {
297 reserve_html(&mut self.prepend, &content);
298 content.escape_and_write(&mut self.prepend);
299 }
300
301 pub fn append(&mut self, content: impl IntoHtml) {
304 reserve_html(&mut self.append, &content);
305 content.escape_and_write(&mut self.append);
306 }
307
308 pub fn after(&mut self, content: impl IntoHtml) {
311 reserve_html(&mut self.after, &content);
312 content.escape_and_write(&mut self.after);
313 }
314
315 pub fn set_inner_content(&mut self, content: impl IntoHtml) {
318 let mut inner = String::with_capacity(html_capacity(&content));
319 content.escape_and_write(&mut inner);
320 self.mode = ElementMode::Inner(inner);
321 }
322
323 pub fn replace(&mut self, content: impl IntoHtml) {
326 let mut replacement = String::with_capacity(html_capacity(&content));
327 content.escape_and_write(&mut replacement);
328 self.mode = ElementMode::Replace(replacement);
329 }
330
331 pub fn remove(&mut self) {
333 self.mode = ElementMode::Remove;
334 }
335
336 pub fn remove_and_keep_content(&mut self) {
339 self.mode = ElementMode::RemoveKeepContent;
340 }
341
342 #[must_use]
346 pub fn is_removed(&self) -> bool {
347 matches!(
348 self.mode,
349 ElementMode::Remove | ElementMode::RemoveKeepContent | ElementMode::Replace(_)
350 )
351 }
352
353 fn ensure_attributes(&mut self) {
354 if self.attributes.is_none() {
355 let attributes = self
356 .tag
357 .attributes()
358 .map(|a| EditedAttribute {
359 name: Cow::Borrowed(a.name()),
360 value: a.has_value().then(|| Cow::Borrowed(a.value())),
361 })
362 .collect();
363 self.attributes = Some(attributes);
364 }
365 }
366
367 pub(crate) fn serialize(self, out: &mut Vec<u8>, visible: bool) -> EndActions {
372 let Self {
373 tag,
374 before,
375 prepend,
376 append,
377 after,
378 attributes,
379 mode,
380 } = self;
381
382 match mode {
383 ElementMode::Normal => {
384 if visible {
385 out.extend_from_slice(before.as_bytes());
386 emit_start_tag(out, tag, attributes.as_deref());
387 out.extend_from_slice(prepend.as_bytes());
388 }
389 EndActions {
390 append,
391 after,
392 suppress_content: false,
393 suppress_end_tag: false,
394 }
395 }
396 ElementMode::Inner(inner) => {
397 if visible {
398 out.extend_from_slice(before.as_bytes());
399 emit_start_tag(out, tag, attributes.as_deref());
400 out.extend_from_slice(prepend.as_bytes());
401 out.extend_from_slice(inner.as_bytes());
402 }
403 EndActions {
404 append,
405 after,
406 suppress_content: true,
407 suppress_end_tag: false,
408 }
409 }
410 ElementMode::Replace(replacement) => {
411 if visible {
412 out.extend_from_slice(before.as_bytes());
413 out.extend_from_slice(replacement.as_bytes());
414 }
415 EndActions {
418 append: String::new(),
419 after,
420 suppress_content: true,
421 suppress_end_tag: true,
422 }
423 }
424 ElementMode::Remove => {
425 if visible {
426 out.extend_from_slice(before.as_bytes());
427 }
428 EndActions {
429 append: String::new(),
430 after,
431 suppress_content: true,
432 suppress_end_tag: true,
433 }
434 }
435 ElementMode::RemoveKeepContent => {
436 if visible {
437 out.extend_from_slice(before.as_bytes());
438 out.extend_from_slice(prepend.as_bytes());
440 }
441 EndActions {
442 append,
443 after,
444 suppress_content: false,
445 suppress_end_tag: true,
446 }
447 }
448 }
449 }
450}
451
452pub(crate) struct EndActions {
455 pub(crate) append: String,
457 pub(crate) after: String,
459 pub(crate) suppress_content: bool,
461 pub(crate) suppress_end_tag: bool,
463}
464
465impl EndActions {
466 pub(crate) fn passthrough() -> Self {
469 Self {
470 append: String::new(),
471 after: String::new(),
472 suppress_content: false,
473 suppress_end_tag: false,
474 }
475 }
476}
477
478fn emit_start_tag(out: &mut Vec<u8>, tag: &StartTag<'_>, edited: Option<&[EditedAttribute<'_>]>) {
481 match edited {
482 None => out.extend_from_slice(tag.raw()),
483 Some(attributes) => {
484 out.push(b'<');
485 out.extend_from_slice(tag.name());
486 for attr in attributes {
487 out.push(b' ');
488 out.extend_from_slice(&attr.name);
489 if let Some(value) = &attr.value {
490 out.extend_from_slice(b"=\"");
491 escape_attr_value_into(out, value);
492 out.push(b'"');
493 }
494 }
495 if tag.is_self_closing() {
496 out.extend_from_slice(b" />");
497 } else {
498 out.push(b'>');
499 }
500 }
501 }
502}
503
504fn reserve_html(buf: &mut String, content: &impl IntoHtml) {
505 buf.reserve(html_capacity(content));
506}
507
508fn html_capacity(content: &impl IntoHtml) -> usize {
509 let hint = content.size_hint();
510 hint + (hint / 10)
511}
512
513#[cfg(test)]
514mod tests {
515 use super::{AttributeName, InvalidAttributeName};
516
517 #[test]
518 fn valid_names() {
519 for name in ["class", "data-x", "aria-label", "x", "ns:attr"] {
520 assert_eq!(AttributeName::from_static(name).as_str(), name);
521 assert_eq!(AttributeName::try_from(name).unwrap().as_str(), name);
522 assert_eq!(name.parse::<AttributeName>().unwrap().as_str(), name);
523 }
524 }
525
526 #[test]
527 fn rejects_invalid_names() {
528 assert_eq!(
529 AttributeName::try_from(""),
530 Err(InvalidAttributeName::Empty)
531 );
532 for (input, byte) in [
534 ("a b", b' '),
535 ("a\tb", b'\t'),
536 ("a=b", b'='),
537 ("a>b", b'>'),
538 ("a/b", b'/'),
539 ("a<b", b'<'),
540 ("a\"b", b'"'),
541 ("a'b", b'\''),
542 ("a\x7fb", 0x7f),
543 ] {
544 assert_eq!(
545 AttributeName::try_from(input),
546 Err(InvalidAttributeName::ForbiddenByte(byte)),
547 "{input:?}"
548 );
549 }
550 }
551
552 #[test]
553 #[should_panic = "invalid HTML attribute name"]
554 fn from_static_panics_on_injection() {
555 let _name = AttributeName::from_static("x onload=alert(1)");
556 }
557}