Skip to main content

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