Skip to main content

feedparser_rs/parser/
mod.rs

1pub mod atom;
2mod common;
3mod context;
4mod detect;
5pub mod json;
6pub mod namespace_detection;
7pub mod rss;
8pub mod rss10;
9
10use crate::{error::Result, types::ParsedFeed};
11
12pub use common::skip_element;
13pub use detect::detect_format;
14
15/// Parse feed from raw bytes
16///
17/// This is the main entry point for parsing feeds. It automatically detects
18/// the feed format (RSS, Atom, JSON) and parses accordingly. Uses
19/// [`crate::ParseOptions::default`], which sanitizes HTML content and resolves
20/// relative URIs.
21///
22/// # Errors
23///
24/// Returns a `FeedError` if the feed cannot be parsed. However, in most cases,
25/// the parser will set the `bozo` flag and return partial results rather than
26/// returning an error.
27///
28/// # Examples
29///
30/// ```
31/// use feedparser_rs::parse;
32///
33/// let xml = r#"
34///     <?xml version="1.0"?>
35///     <rss version="2.0">
36///         <channel>
37///             <title>Example Feed</title>
38///         </channel>
39///     </rss>
40/// "#;
41///
42/// let feed = parse(xml.as_bytes()).unwrap();
43/// assert_eq!(feed.feed.title.as_deref(), Some("Example Feed"));
44/// ```
45pub fn parse(data: &[u8]) -> Result<ParsedFeed> {
46    parse_with_options(data, &crate::ParseOptions::default())
47}
48
49/// Parse feed with custom parser limits
50///
51/// This allows controlling resource usage when parsing untrusted feeds. HTML
52/// sanitization and relative URI resolution use their [`crate::ParseOptions::default`]
53/// settings (both enabled); use [`parse_with_options`] to control them directly.
54///
55/// # Examples
56///
57/// ```
58/// use feedparser_rs::{parse_with_limits, ParserLimits};
59///
60/// let xml = b"<rss version=\"2.0\"><channel><title>Test</title></channel></rss>";
61/// let limits = ParserLimits::strict();
62/// let feed = parse_with_limits(xml, limits).unwrap();
63/// ```
64///
65/// # Errors
66///
67/// Returns an error if:
68/// - Feed size exceeds limits
69/// - Format is unknown or unsupported
70/// - Fatal parsing error occurs
71pub fn parse_with_limits(data: &[u8], limits: crate::ParserLimits) -> Result<ParsedFeed> {
72    parse_with_options(
73        data,
74        &crate::ParseOptions {
75            limits,
76            ..crate::ParseOptions::default()
77        },
78    )
79}
80
81/// Parse feed from raw bytes with full control over parser behavior
82///
83/// This is the most flexible entry point: [`parse`] and [`parse_with_limits`] are
84/// thin wrappers around it. Controls HTML sanitization, relative URI resolution,
85/// and resource limits via [`crate::ParseOptions`].
86///
87/// # Examples
88///
89/// ```
90/// use feedparser_rs::{parse_with_options, ParseOptions};
91///
92/// // Trust the feed source and skip sanitization
93/// let options = ParseOptions {
94///     sanitize_html: false,
95///     ..ParseOptions::default()
96/// };
97/// let xml = b"<rss version=\"2.0\"><channel><title>Test</title></channel></rss>";
98/// let feed = parse_with_options(xml, &options).unwrap();
99/// ```
100///
101/// # Errors
102///
103/// Returns an error if:
104/// - Feed size exceeds limits
105/// - Format is unknown or unsupported
106/// - Fatal parsing error occurs
107pub fn parse_with_options(data: &[u8], options: &crate::ParseOptions) -> Result<ParsedFeed> {
108    use crate::types::FeedVersion;
109    use crate::util::encoding::detect_and_convert;
110
111    let limits = options.limits;
112    let resolve_relative_uris = options.resolve_relative_uris;
113
114    // Detect encoding and convert to UTF-8 before parsing.
115    // This handles ISO-8859-1, Windows-1252, UTF-16, and BOM-prefixed feeds.
116    let (utf8_string, detected_encoding) = detect_and_convert(data)
117        .unwrap_or_else(|_| (String::from_utf8_lossy(data).into_owned(), "UTF-8"));
118
119    let utf8_bytes = utf8_string.as_bytes();
120    let encoding_label = detected_encoding.to_lowercase();
121
122    // Detect format on UTF-8 data (required for correct UTF-16 detection)
123    let version = detect_format(utf8_bytes);
124
125    // Parse based on detected format, then update the encoding field
126    let mut feed = match version {
127        // RSS variants (all use RSS 2.0 parser; overwrite version after parsing)
128        FeedVersion::Rss20
129        | FeedVersion::Rss092
130        | FeedVersion::Rss091Netscape
131        | FeedVersion::Rss091Userland
132        | FeedVersion::Rss090 => {
133            let mut parsed =
134                rss::parse_rss20_with_options(utf8_bytes, limits, resolve_relative_uris)?;
135            parsed.version = version;
136            Ok(parsed)
137        }
138
139        // Atom variants
140        FeedVersion::Atom10 | FeedVersion::Atom03 => {
141            atom::parse_atom10_with_options(utf8_bytes, limits, resolve_relative_uris)
142        }
143
144        // RSS 1.0 (RDF)
145        FeedVersion::Rss10 => {
146            rss10::parse_rss10_with_options(utf8_bytes, limits, resolve_relative_uris)
147        }
148
149        // JSON Feed: `resolve_relative_uris` is intentionally not threaded through
150        // here — the JSON Feed parser does not resolve relative URIs against a
151        // base URL at all (JSON Feed has no `xml:base`-equivalent concept), so
152        // there is nothing for the option to gate.
153        FeedVersion::JsonFeed10 | FeedVersion::JsonFeed11 => {
154            json::parse_json_feed_with_limits(utf8_bytes, limits)
155        }
156
157        // Unknown format - return a bozo feed since the format is unrecognized.
158        // The bozo pattern requires we never panic and always return partial data,
159        // but unrecognizable input must signal the caller via bozo=true.
160        FeedVersion::Unknown => {
161            let mut feed = crate::types::ParsedFeed::new();
162            feed.version = FeedVersion::Unknown;
163            feed.bozo = true;
164            feed.bozo_exception = Some("Feed format not recognized".to_string());
165            Ok(feed)
166        }
167    }?;
168
169    feed.encoding = encoding_label;
170
171    if options.sanitize_html {
172        crate::util::sanitize::sanitize_feed(&mut feed, &limits);
173    }
174
175    Ok(feed)
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    #[test]
183    fn test_parse_returns_ok_bozo_for_garbage() {
184        let feed = parse(b"test").unwrap();
185        assert!(feed.bozo, "unrecognized input must set bozo");
186        assert_eq!(feed.version, crate::types::FeedVersion::Unknown);
187        assert!(feed.entries.is_empty());
188    }
189
190    #[test]
191    fn test_rss091n_version_string() {
192        // #283: RSS 0.91 with Netscape DOCTYPE must report "rss091n"
193        let xml = br#"<?xml version="1.0"?>
194<!DOCTYPE rss PUBLIC "-//Netscape Communications//DTD RSS 0.91//EN"
195    "http://my.netscape.com/publish/formats/rss-0.91.dtd">
196<rss version="0.91">
197<channel><title>T</title><link>http://example.com</link><description>D</description>
198<language>en</language></channel></rss>"#;
199        let feed = parse(xml).unwrap();
200        assert_eq!(feed.version.as_str(), "rss091n");
201    }
202
203    #[test]
204    fn test_rss091u_version_string() {
205        // #283: RSS 0.91 without Netscape DOCTYPE must report "rss091u"
206        let xml = br#"<?xml version="1.0"?>
207<rss version="0.91">
208<channel><title>T</title><link>http://example.com</link><description>D</description>
209<language>en</language></channel></rss>"#;
210        let feed = parse(xml).unwrap();
211        assert_eq!(feed.version.as_str(), "rss091u");
212    }
213
214    #[test]
215    fn test_rss092_version_string() {
216        // #283: RSS 0.92 feeds must report version "rss092", not "rss20"
217        let xml = br#"<?xml version="1.0"?>
218<rss version="0.92">
219<channel><title>T</title><link>http://example.com</link><description>D</description>
220</channel></rss>"#;
221        let feed = parse(xml).unwrap();
222        assert_eq!(feed.version.as_str(), "rss092");
223    }
224
225    #[test]
226    fn test_rss20_version_string_unchanged() {
227        // #283: RSS 2.0 feeds must still report version "rss20"
228        let xml = br#"<?xml version="1.0"?>
229<rss version="2.0">
230<channel><title>T</title><link>http://example.com</link><description>D</description>
231</channel></rss>"#;
232        let feed = parse(xml).unwrap();
233        assert_eq!(feed.version.as_str(), "rss20");
234    }
235}