Skip to main content

feed_parser/parsers/rss2/
mod.rs

1use crate::parsers::{
2    Feed,
3    errors::{ParseError, ParseResult},
4    internal::{TextRun, compile_tag_patterns, deserialize_feed, escape_inline_html},
5};
6use core::str;
7use quick_xml::Reader;
8use quick_xml::Writer;
9use quick_xml::events::{BytesEnd, BytesStart, Event};
10use regex::Regex;
11use std::io::Cursor;
12use std::sync::LazyLock;
13
14#[cfg(test)]
15mod tests;
16
17/// Elements whose content may contain unescaped HTML.
18static HTML_BEARING_TAGS: LazyLock<Vec<(&'static str, Regex)>> = LazyLock::new(|| {
19    compile_tag_patterns(&["title", "description", "summary", "content", "other"])
20});
21
22/// Parses an RSS 2.0 document into one [`Feed`] per `<item>`.
23///
24/// # Errors
25///
26/// Returns [`ParseError::XmlParseError`] if the document is not well-formed XML,
27/// [`ParseError::MissingField`] if an item lacks a required field, and
28/// [`ParseError::InvalidFeedFormat`] if an item is never closed.
29pub fn parse(text: &str) -> ParseResult<Vec<Feed>> {
30    let text = escape_inline_html(text, &HTML_BEARING_TAGS);
31
32    let mut reader = Reader::from_str(&text);
33
34    let mut feeds = Vec::new();
35    let mut writer = Writer::new(Cursor::new(Vec::new()));
36    // Character data is buffered rather than written straight through; see
37    // `TextRun` for why the reader's own `trim_text` cannot be used here.
38    let mut run = TextRun::default();
39    let mut parsing = false;
40    loop {
41        match reader.read_event()? {
42            Event::Start(e) => {
43                run.flush(&mut writer)?;
44                if parsing {
45                    if e.name().as_ref() == b"dc:creator" {
46                        writer.write_event(Event::Start(BytesStart::new("creator")))?;
47                    } else if e.name().as_ref() == b"dc:date" {
48                        writer.write_event(Event::Start(BytesStart::new("date")))?;
49                    } else if e.name().as_ref() == b"pubDate" {
50                        writer.write_event(Event::Start(BytesStart::new("publish_date")))?;
51                    } else {
52                        writer.write_event(Event::Start(e.clone()))?;
53                    }
54                }
55                if e.name().as_ref() == b"item" {
56                    writer.write_event(Event::Start(BytesStart::new("item")))?;
57                    parsing = true;
58                }
59            }
60            Event::End(e) => {
61                run.flush(&mut writer)?;
62                if parsing && e.name().as_ref() == b"item" {
63                    writer.write_event(Event::End(BytesEnd::new("item")))?;
64                    let feed_text = writer.into_inner().into_inner();
65                    feeds.push(deserialize_feed(str::from_utf8(&feed_text)?)?);
66
67                    writer = Writer::new(Cursor::new(Vec::new()));
68                    parsing = false;
69                } else if parsing {
70                    if e.name().as_ref() == b"dc:creator" {
71                        writer.write_event(Event::End(BytesEnd::new("creator")))?;
72                    } else if e.name().as_ref() == b"dc:date" {
73                        writer.write_event(Event::End(BytesEnd::new("date")))?;
74                    } else if e.name().as_ref() == b"pubDate" {
75                        writer.write_event(Event::End(BytesEnd::new("publish_date")))?;
76                    } else {
77                        writer.write_event(Event::End(e))?;
78                    }
79                }
80            }
81            Event::Text(e) => {
82                if parsing {
83                    run.push(Event::Text(e));
84                }
85            }
86            Event::CData(e) => {
87                if parsing {
88                    run.push(Event::CData(e));
89                }
90            }
91            // quick-xml reports entity references as their own event. Dropping
92            // them would silently delete every escaped character in the entry.
93            Event::GeneralRef(e) => {
94                if parsing {
95                    run.push(Event::GeneralRef(e));
96                }
97            }
98            Event::Eof => break,
99            _ => {}
100        }
101    }
102
103    if parsing {
104        return Err(ParseError::InvalidFeedFormat(
105            "reached end of document inside an unclosed <item> element".to_string(),
106        ));
107    }
108
109    Ok(feeds)
110}