Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:
run: cargo +nightly fmt --all -- --check

- name: Clippy
run: cargo clippy --all-targets --all-features -- -D warnings
run: cargo +stable clippy --all-targets --all-features -- -D warnings

- name: Check documentation
run: cargo doc --no-deps --all-features
Expand Down Expand Up @@ -97,7 +97,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: [18, 20, 22]
node: [20, 22]

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -162,24 +162,24 @@ jobs:

# MSRV check
msrv:
name: Check MSRV (1.86.0)
name: Check MSRV (1.88.0)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4

- name: Install Rust 1.86.0
- name: Install Rust 1.88.0
uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.86.0"
toolchain: "1.88.0"

- name: Cache Cargo
uses: Swatinem/rust-cache@v2
with:
shared-key: "msrv"

- name: Check with MSRV
run: cargo +1.86.0 check --all-features
run: cargo +1.88.0 check --all-features

# All checks passed gate
ci-success:
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ resolver = "2"
[workspace.package]
version = "0.1.0"
edition = "2024"
rust-version = "1.86.0"
rust-version = "1.88.0"
authors = ["bug-ops"]
license = "MIT OR Apache-2.0"
repository = "https://github.com/bug-ops/feedparser-rs"
Expand Down
6 changes: 4 additions & 2 deletions crates/feedparser-rs-core/benches/parsing.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#![allow(missing_docs)]

use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
use feedparser_rs_core::parse;
use std::hint::black_box;
Expand All @@ -10,7 +12,7 @@ fn bench_parse_feeds(c: &mut Criterion) {
let mut group = c.benchmark_group("parse");

group.bench_with_input(BenchmarkId::new("rss", "small"), &SMALL_FEED, |b, data| {
b.iter(|| parse(black_box(data)))
b.iter(|| parse(black_box(data)));
});

group.bench_with_input(
Expand All @@ -20,7 +22,7 @@ fn bench_parse_feeds(c: &mut Criterion) {
);

group.bench_with_input(BenchmarkId::new("rss", "large"), &LARGE_FEED, |b, data| {
b.iter(|| parse(black_box(data)))
b.iter(|| parse(black_box(data)));
});

group.finish();
Expand Down
17 changes: 4 additions & 13 deletions crates/feedparser-rs-core/src/compat/mod.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
/// Compatibility utilities for feedparser API
///
/// This module provides utilities to ensure API compatibility with
/// Python's feedparser library.
// Compatibility utilities for feedparser API
//
// This module provides utilities to ensure API compatibility with
// Python's feedparser library.

// TODO: Implement in later phases as needed

#[cfg(test)]
mod tests {
#[test]
fn test_placeholder() {
// Placeholder test
assert!(true);
}
}
9 changes: 7 additions & 2 deletions crates/feedparser-rs-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,14 @@ mod tests {
}

#[test]
#[allow(clippy::unnecessary_wraps)]
fn test_result_type() {
let result: Result<i32> = Ok(42);
assert_eq!(result.unwrap(), 42);
fn get_result() -> Result<i32> {
Ok(42)
}
let result = get_result();
assert!(result.is_ok());
assert_eq!(result.expect("should be ok"), 42);

let error: Result<i32> = Err(FeedError::Unknown("test".to_string()));
assert!(error.is_err());
Expand Down
39 changes: 18 additions & 21 deletions crates/feedparser-rs-core/src/parser/atom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,16 +101,15 @@ fn parse_feed_element(
match reader.read_event_into(&mut buf) {
Ok(event @ (Event::Start(_) | Event::Empty(_))) => {
let is_empty = matches!(event, Event::Empty(_));
let e = match &event {
Event::Start(e) | Event::Empty(e) => e,
_ => unreachable!(),
let (Event::Start(e) | Event::Empty(e)) = &event else {
unreachable!()
};

*depth += 1;
if *depth > limits.max_nesting_depth {
return Err(FeedError::InvalidFormat(format!(
"XML nesting depth {} exceeds maximum {}",
depth, limits.max_nesting_depth
"XML nesting depth {depth} exceeds maximum {}",
limits.max_nesting_depth
)));
}

Expand Down Expand Up @@ -153,7 +152,7 @@ fn parse_feed_element(
b"author" if !is_empty => {
if let Ok(person) = parse_person(reader, &mut buf, limits, depth) {
if feed.feed.author.is_none() {
feed.feed.author = person.name.clone();
feed.feed.author.clone_from(&person.name);
feed.feed.author_detail = Some(person.clone());
}
feed.feed
Expand Down Expand Up @@ -200,7 +199,7 @@ fn parse_feed_element(
feed.bozo = true;
feed.bozo_exception =
Some(format!("Entry limit exceeded: {}", limits.max_entries));
skip_element(reader, &mut buf, limits, depth)?;
skip_element(reader, &mut buf, limits, *depth)?;
*depth = depth.saturating_sub(1);
continue;
}
Expand All @@ -215,7 +214,7 @@ fn parse_feed_element(
}
_ => {
if !is_empty {
skip_element(reader, &mut buf, limits, depth)?;
skip_element(reader, &mut buf, limits, *depth)?;
}
}
}
Expand Down Expand Up @@ -246,16 +245,15 @@ fn parse_entry(
match reader.read_event_into(buf) {
Ok(event @ (Event::Start(_) | Event::Empty(_))) => {
let is_empty = matches!(event, Event::Empty(_));
let e = match &event {
Event::Start(e) | Event::Empty(e) => e,
_ => unreachable!(),
let (Event::Start(e) | Event::Empty(e)) = &event else {
unreachable!()
};

*depth += 1;
if *depth > limits.max_nesting_depth {
return Err(FeedError::InvalidFormat(format!(
"XML nesting depth {} exceeds maximum {}",
depth, limits.max_nesting_depth
"XML nesting depth {depth} exceeds maximum {}",
limits.max_nesting_depth
)));
}

Expand Down Expand Up @@ -307,7 +305,7 @@ fn parse_entry(
b"author" if !is_empty => {
if let Ok(person) = parse_person(reader, buf, limits, depth) {
if entry.author.is_none() {
entry.author = person.name.clone();
entry.author.clone_from(&person.name);
entry.author_detail = Some(person.clone());
}
entry.authors.try_push_limited(person, limits.max_authors);
Expand Down Expand Up @@ -338,7 +336,7 @@ fn parse_entry(
}
_ => {
if !is_empty {
skip_element(reader, buf, limits, depth)?;
skip_element(reader, buf, limits, *depth)?;
}
}
}
Expand Down Expand Up @@ -414,7 +412,7 @@ fn parse_person(
b"name" => name = Some(read_text(reader, buf, limits)?),
b"email" => email = Some(read_text(reader, buf, limits)?),
b"uri" => uri = Some(read_text(reader, buf, limits)?),
_ => skip_element(reader, buf, limits, depth)?,
_ => skip_element(reader, buf, limits, *depth)?,
}
*depth = depth.saturating_sub(1);
}
Expand Down Expand Up @@ -517,15 +515,14 @@ fn parse_atom_source(
if let Some(l) = Link::from_attributes(
element.attributes().flatten(),
limits.max_attribute_length,
) {
if link.is_none() {
link = Some(l.href);
}
) && link.is_none()
{
link = Some(l.href);
}
skip_to_end(reader, buf, b"link")?;
}
b"id" => id = Some(read_text(reader, buf, limits)?),
_ => skip_element(reader, buf, limits, depth)?,
_ => skip_element(reader, buf, limits, *depth)?,
}
*depth = depth.saturating_sub(1);
}
Expand Down
31 changes: 17 additions & 14 deletions crates/feedparser-rs-core/src/parser/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ pub struct ParseContext<'a> {

impl<'a> ParseContext<'a> {
/// Create a new parse context from raw data
#[allow(dead_code)]
pub fn new(data: &'a [u8], limits: ParserLimits) -> Result<Self> {
limits
.check_feed_size(data.len())
Expand All @@ -54,6 +55,7 @@ impl<'a> ParseContext<'a> {

/// Check and increment depth, returning error if limit exceeded
#[inline]
#[allow(dead_code)]
pub fn check_depth(&mut self) -> Result<()> {
self.depth += 1;
if self.depth > self.limits.max_nesting_depth {
Expand All @@ -67,18 +69,20 @@ impl<'a> ParseContext<'a> {

/// Decrement depth safely
#[inline]
pub fn decrement_depth(&mut self) {
#[allow(dead_code)]
pub const fn decrement_depth(&mut self) {
self.depth = self.depth.saturating_sub(1);
}

/// Clear the buffer
#[inline]
#[allow(dead_code)]
pub fn clear_buf(&mut self) {
self.buf.clear();
}
}

/// Initialize a ParsedFeed with common setup for any format
/// Initialize a `ParsedFeed` with common setup for any format
#[inline]
pub fn init_feed(version: FeedVersion, max_entries: usize) -> ParsedFeed {
let mut feed = ParsedFeed::with_capacity(max_entries);
Expand All @@ -89,15 +93,14 @@ pub fn init_feed(version: FeedVersion, max_entries: usize) -> ParsedFeed {

/// Check nesting depth and return error if exceeded
///
/// This is a standalone helper for parsers that don't use ParseContext.
/// Future use: Will be used when ParseContext is adopted project-wide
/// This is a standalone helper for parsers that don't use `ParseContext`.
/// Future use: Will be used when `ParseContext` is adopted project-wide
#[inline]
#[allow(dead_code)]
pub fn check_depth(depth: usize, max_depth: usize) -> Result<()> {
if depth > max_depth {
return Err(FeedError::InvalidFormat(format!(
"XML nesting depth {} exceeds maximum {}",
depth, max_depth
"XML nesting depth {depth} exceeds maximum {max_depth}"
)));
}
Ok(())
Expand All @@ -109,10 +112,10 @@ pub fn check_depth(depth: usize, max_depth: usize) -> Result<()> {
/// is valid UTF-8, falling back to lossy conversion otherwise.
#[inline]
pub fn bytes_to_string(value: &[u8]) -> String {
match std::str::from_utf8(value) {
Ok(s) => s.to_string(),
Err(_) => String::from_utf8_lossy(value).into_owned(),
}
std::str::from_utf8(value).map_or_else(
|_| String::from_utf8_lossy(value).into_owned(),
std::string::ToString::to_string,
)
}

/// Read text content from current XML element (handles text and CDATA)
Expand Down Expand Up @@ -160,15 +163,15 @@ pub fn skip_element(
reader: &mut Reader<&[u8]>,
buf: &mut Vec<u8>,
limits: &ParserLimits,
current_depth: &mut usize,
current_depth: usize,
) -> Result<()> {
let mut local_depth: usize = 1;

loop {
match reader.read_event_into(buf) {
Ok(Event::Start(_)) => {
local_depth += 1;
if *current_depth + local_depth > limits.max_nesting_depth {
if current_depth + local_depth > limits.max_nesting_depth {
return Err(FeedError::InvalidFormat(format!(
"XML nesting depth exceeds maximum of {}",
limits.max_nesting_depth
Expand Down Expand Up @@ -278,7 +281,7 @@ mod tests {
reader.config_mut().trim_text(true);
let mut buf = Vec::new();
let limits = ParserLimits::default();
let mut depth = 1;
let depth = 1;

// Skip to after the start tag
loop {
Expand All @@ -291,7 +294,7 @@ mod tests {
}
buf.clear();

let result = skip_element(&mut reader, &mut buf, &limits, &mut depth);
let result = skip_element(&mut reader, &mut buf, &limits, depth);
assert!(result.is_ok());
}
}
20 changes: 10 additions & 10 deletions crates/feedparser-rs-core/src/parser/detect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,14 @@ fn detect_json_feed_version(data: &[u8]) -> FeedVersion {
}

// Try to parse as JSON and check version field
if let Ok(json) = serde_json::from_slice::<serde_json::Value>(data) {
if let Some(version) = json.get("version").and_then(|v| v.as_str()) {
return match version {
"https://jsonfeed.org/version/1" => FeedVersion::JsonFeed10,
"https://jsonfeed.org/version/1.1" => FeedVersion::JsonFeed11,
_ => FeedVersion::Unknown,
};
}
if let Ok(json) = serde_json::from_slice::<serde_json::Value>(data)
&& let Some(version) = json.get("version").and_then(|v| v.as_str())
{
return match version {
"https://jsonfeed.org/version/1" => FeedVersion::JsonFeed10,
"https://jsonfeed.org/version/1.1" => FeedVersion::JsonFeed11,
_ => FeedVersion::Unknown,
};
}
FeedVersion::Unknown
}
Expand Down Expand Up @@ -209,7 +209,7 @@ mod tests {

#[test]
fn test_detect_atom10_no_xmlns() {
let xml = br#"<feed></feed>"#;
let xml = br"<feed></feed>";
assert_eq!(detect_format(xml), FeedVersion::Atom10);
}

Expand All @@ -233,7 +233,7 @@ mod tests {

#[test]
fn test_detect_unknown_xml() {
let xml = br#"<unknown></unknown>"#;
let xml = br"<unknown></unknown>";
assert_eq!(detect_format(xml), FeedVersion::Unknown);
}

Expand Down
Loading