Skip to content

Commit 5ade586

Browse files
authored
Merge pull request #12 from bug-ops/feat/phase-5.1-http-bindings
feat: Phase 5.1 - HTTP bindings and Podcast 2.0 support
2 parents e3d1dfe + 654f612 commit 5ade586

File tree

12 files changed

+1629
-36
lines changed

12 files changed

+1629
-36
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ jobs:
398398

399399
- name: Run tests with coverage
400400
working-directory: crates/feedparser-rs-node
401-
run: npm test -- --coverage
401+
run: npm run test:coverage
402402
continue-on-error: true
403403

404404
- name: Upload Node.js coverage to codecov

crates/feedparser-rs-core/src/http/client.rs

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,19 @@ impl FeedHttpClient {
5151
}
5252

5353
/// Sets a custom User-Agent header
54+
///
55+
/// # Security
56+
///
57+
/// User-Agent is truncated to 512 bytes to prevent header injection attacks.
5458
#[must_use]
5559
pub fn with_user_agent(mut self, agent: String) -> Self {
56-
self.user_agent = agent;
60+
// Truncate to 512 bytes to prevent header injection
61+
const MAX_USER_AGENT_LEN: usize = 512;
62+
self.user_agent = if agent.len() > MAX_USER_AGENT_LEN {
63+
agent.chars().take(MAX_USER_AGENT_LEN).collect()
64+
} else {
65+
agent
66+
};
5767
self
5868
}
5969

@@ -125,16 +135,30 @@ impl FeedHttpClient {
125135
HeaderValue::from_static("gzip, deflate, br"),
126136
);
127137

128-
// Conditional GET headers
138+
// Conditional GET headers with length validation
129139
if let Some(etag_val) = etag {
130-
Self::insert_header(&mut headers, IF_NONE_MATCH, etag_val, "ETag")?;
140+
// Truncate ETag to 1KB to prevent oversized headers
141+
const MAX_ETAG_LEN: usize = 1024;
142+
let sanitized_etag = if etag_val.len() > MAX_ETAG_LEN {
143+
&etag_val[..MAX_ETAG_LEN]
144+
} else {
145+
etag_val
146+
};
147+
Self::insert_header(&mut headers, IF_NONE_MATCH, sanitized_etag, "ETag")?;
131148
}
132149

133150
if let Some(modified_val) = modified {
151+
// Truncate Last-Modified to 64 bytes (RFC 822 dates are ~30 bytes)
152+
const MAX_MODIFIED_LEN: usize = 64;
153+
let sanitized_modified = if modified_val.len() > MAX_MODIFIED_LEN {
154+
&modified_val[..MAX_MODIFIED_LEN]
155+
} else {
156+
modified_val
157+
};
134158
Self::insert_header(
135159
&mut headers,
136160
IF_MODIFIED_SINCE,
137-
modified_val,
161+
sanitized_modified,
138162
"Last-Modified",
139163
)?;
140164
}
@@ -161,8 +185,8 @@ impl FeedHttpClient {
161185
let status = response.status().as_u16();
162186
let url = response.url().to_string();
163187

164-
// Convert headers to HashMap
165-
let mut headers_map = HashMap::new();
188+
// Convert headers to HashMap with pre-allocated capacity
189+
let mut headers_map = HashMap::with_capacity(response.headers().len());
166190
for (name, value) in response.headers() {
167191
if let Ok(val_str) = value.to_str() {
168192
headers_map.insert(name.to_string(), val_str.to_string());

crates/feedparser-rs-core/src/parser/rss.rs

Lines changed: 212 additions & 17 deletions
Large diffs are not rendered by default.

crates/feedparser-rs-core/src/types/entry.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use super::{
22
common::{
33
Content, Enclosure, Link, MediaContent, MediaThumbnail, Person, Source, Tag, TextConstruct,
44
},
5-
podcast::ItunesEntryMeta,
5+
podcast::{ItunesEntryMeta, PodcastPerson, PodcastTranscript},
66
};
77
use chrono::{DateTime, Utc};
88

@@ -67,6 +67,10 @@ pub struct Entry {
6767
pub media_thumbnails: Vec<MediaThumbnail>,
6868
/// Media RSS content items
6969
pub media_content: Vec<MediaContent>,
70+
/// Podcast 2.0 transcripts for this episode
71+
pub podcast_transcripts: Vec<PodcastTranscript>,
72+
/// Podcast 2.0 persons for this episode (hosts, guests, etc.)
73+
pub podcast_persons: Vec<PodcastPerson>,
7074
}
7175

7276
impl Entry {
@@ -78,6 +82,8 @@ impl Entry {
7882
/// - 1 author
7983
/// - 2-3 tags
8084
/// - 0-1 enclosures
85+
/// - 2 podcast transcripts (typical for podcasts with multiple languages)
86+
/// - 4 podcast persons (host, co-hosts, guests)
8187
///
8288
/// # Examples
8389
///
@@ -98,6 +104,8 @@ impl Entry {
98104
dc_subject: Vec::with_capacity(2),
99105
media_thumbnails: Vec::with_capacity(1),
100106
media_content: Vec::with_capacity(1),
107+
podcast_transcripts: Vec::with_capacity(2),
108+
podcast_persons: Vec::with_capacity(4),
101109
..Default::default()
102110
}
103111
}

crates/feedparser-rs-node/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,9 @@ feedparser-rs-core = { path = "../feedparser-rs-core" }
1717
napi = { workspace = true, features = ["napi9", "error_anyhow"] }
1818
napi-derive = { workspace = true }
1919

20+
[features]
21+
default = ["http"]
22+
http = ["feedparser-rs-core/http"]
23+
2024
[build-dependencies]
2125
napi-build = "2.1"

0 commit comments

Comments
 (0)