#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ParserLimits {
pub max_entries: usize,
pub max_links_per_feed: usize,
pub max_links_per_entry: usize,
pub max_authors: usize,
pub max_contributors: usize,
pub max_tags: usize,
pub max_content_blocks: usize,
pub max_enclosures: usize,
pub max_namespaces: usize,
pub max_nesting_depth: usize,
pub max_text_length: usize,
pub max_feed_size_bytes: usize,
pub max_attribute_length: usize,
pub max_podcast_soundbites: usize,
pub max_podcast_transcripts: usize,
pub max_podcast_funding: usize,
pub max_podcast_persons: usize,
pub max_value_recipients: usize,
pub max_podcast_alternate_enclosures: usize,
pub max_podcast_alternate_enclosure_sources: usize,
pub max_podcast_podroll: usize,
pub max_podcast_social_interact: usize,
pub max_podcast_txt: usize,
pub max_podcast_follow: usize,
}
impl Default for ParserLimits {
fn default() -> Self {
Self {
max_entries: 10_000,
max_links_per_feed: 100,
max_links_per_entry: 50,
max_authors: 20,
max_contributors: 20,
max_tags: 100,
max_content_blocks: 10,
max_enclosures: 20,
max_namespaces: 100,
max_nesting_depth: 100,
max_text_length: 10 * 1024 * 1024, max_feed_size_bytes: 100 * 1024 * 1024, max_attribute_length: 64 * 1024, max_podcast_soundbites: 10,
max_podcast_transcripts: 20,
max_podcast_funding: 20,
max_podcast_persons: 50,
max_value_recipients: 20,
max_podcast_alternate_enclosures: 20,
max_podcast_alternate_enclosure_sources: 10,
max_podcast_podroll: 50,
max_podcast_social_interact: 20,
max_podcast_txt: 20,
max_podcast_follow: 20,
}
}
}
impl ParserLimits {
#[must_use]
pub const fn strict() -> Self {
Self {
max_entries: 1_000,
max_links_per_feed: 20,
max_links_per_entry: 10,
max_authors: 5,
max_contributors: 5,
max_tags: 20,
max_content_blocks: 3,
max_enclosures: 5,
max_namespaces: 20,
max_nesting_depth: 50,
max_text_length: 1024 * 1024, max_feed_size_bytes: 10 * 1024 * 1024, max_attribute_length: 8 * 1024, max_podcast_soundbites: 5,
max_podcast_transcripts: 5,
max_podcast_funding: 5,
max_podcast_persons: 10,
max_value_recipients: 5,
max_podcast_alternate_enclosures: 5,
max_podcast_alternate_enclosure_sources: 3,
max_podcast_podroll: 10,
max_podcast_social_interact: 5,
max_podcast_txt: 5,
max_podcast_follow: 5,
}
}
#[must_use]
pub const fn permissive() -> Self {
Self {
max_entries: 100_000,
max_links_per_feed: 500,
max_links_per_entry: 200,
max_authors: 100,
max_contributors: 100,
max_tags: 500,
max_content_blocks: 50,
max_enclosures: 100,
max_namespaces: 500,
max_nesting_depth: 200,
max_text_length: 50 * 1024 * 1024, max_feed_size_bytes: 500 * 1024 * 1024, max_attribute_length: 256 * 1024, max_podcast_soundbites: 50,
max_podcast_transcripts: 100,
max_podcast_funding: 50,
max_podcast_persons: 200,
max_value_recipients: 50,
max_podcast_alternate_enclosures: 100,
max_podcast_alternate_enclosure_sources: 50,
max_podcast_podroll: 200,
max_podcast_social_interact: 100,
max_podcast_txt: 100,
max_podcast_follow: 100,
}
}
pub const fn check_feed_size(&self, size: usize) -> Result<(), LimitError> {
if size > self.max_feed_size_bytes {
Err(LimitError::FeedTooLarge {
size,
max: self.max_feed_size_bytes,
})
} else {
Ok(())
}
}
pub const fn check_collection_size(
&self,
current: usize,
limit: usize,
name: &'static str,
) -> Result<(), LimitError> {
if current >= limit {
Err(LimitError::CollectionTooLarge {
name,
size: current,
max: limit,
})
} else {
Ok(())
}
}
pub const fn check_nesting_depth(&self, depth: usize) -> Result<(), LimitError> {
if depth > self.max_nesting_depth {
Err(LimitError::NestingTooDeep {
depth,
max: self.max_nesting_depth,
})
} else {
Ok(())
}
}
pub const fn check_text_length(&self, length: usize) -> Result<(), LimitError> {
if length > self.max_text_length {
Err(LimitError::TextTooLong {
length,
max: self.max_text_length,
})
} else {
Ok(())
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[allow(missing_docs)] pub enum LimitError {
#[error("Feed size ({size} bytes) exceeds maximum ({max} bytes)")]
FeedTooLarge { size: usize, max: usize },
#[error("Collection '{name}' has {size} items, exceeds maximum ({max})")]
CollectionTooLarge {
name: &'static str,
size: usize,
max: usize,
},
#[error("XML nesting depth ({depth}) exceeds maximum ({max})")]
NestingTooDeep { depth: usize, max: usize },
#[error("Text field length ({length} bytes) exceeds maximum ({max} bytes)")]
TextTooLong { length: usize, max: usize },
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_limits() {
let limits = ParserLimits::default();
assert_eq!(limits.max_entries, 10_000);
assert_eq!(limits.max_feed_size_bytes, 100 * 1024 * 1024);
}
#[test]
fn test_strict_limits() {
let limits = ParserLimits::strict();
assert_eq!(limits.max_entries, 1_000);
assert!(limits.max_entries < ParserLimits::default().max_entries);
}
#[test]
fn test_permissive_limits() {
let limits = ParserLimits::permissive();
assert_eq!(limits.max_entries, 100_000);
assert!(limits.max_entries > ParserLimits::default().max_entries);
}
#[test]
fn test_check_feed_size_ok() {
let limits = ParserLimits::default();
assert!(limits.check_feed_size(1024).is_ok());
}
#[test]
fn test_check_feed_size_too_large() {
let limits = ParserLimits::default();
let result = limits.check_feed_size(200 * 1024 * 1024);
assert!(result.is_err());
assert!(matches!(result, Err(LimitError::FeedTooLarge { .. })));
}
#[test]
fn test_check_collection_size_ok() {
let limits = ParserLimits::default();
assert!(
limits
.check_collection_size(50, limits.max_entries, "entries")
.is_ok()
);
}
#[test]
fn test_check_collection_size_too_large() {
let limits = ParserLimits::default();
let result = limits.check_collection_size(10_001, limits.max_entries, "entries");
assert!(result.is_err());
assert!(matches!(result, Err(LimitError::CollectionTooLarge { .. })));
}
#[test]
fn test_check_nesting_depth_ok() {
let limits = ParserLimits::default();
assert!(limits.check_nesting_depth(50).is_ok());
}
#[test]
fn test_check_nesting_depth_too_deep() {
let limits = ParserLimits::default();
let result = limits.check_nesting_depth(101);
assert!(result.is_err());
assert!(matches!(result, Err(LimitError::NestingTooDeep { .. })));
}
#[test]
fn test_check_text_length_ok() {
let limits = ParserLimits::default();
assert!(limits.check_text_length(1024).is_ok());
}
#[test]
fn test_check_text_length_too_long() {
let limits = ParserLimits::default();
let result = limits.check_text_length(20 * 1024 * 1024);
assert!(result.is_err());
assert!(matches!(result, Err(LimitError::TextTooLong { .. })));
}
#[test]
fn test_limit_error_display() {
let err = LimitError::FeedTooLarge {
size: 200_000_000,
max: 100_000_000,
};
let msg = err.to_string();
assert!(msg.contains("200000000"));
assert!(msg.contains("100000000"));
}
#[test]
fn test_max_value_recipients_default() {
let limits = ParserLimits::default();
assert_eq!(limits.max_value_recipients, 20);
}
#[test]
fn test_max_value_recipients_strict() {
let limits = ParserLimits::strict();
assert_eq!(limits.max_value_recipients, 5);
assert!(limits.max_value_recipients < ParserLimits::default().max_value_recipients);
}
#[test]
fn test_max_value_recipients_permissive() {
let limits = ParserLimits::permissive();
assert_eq!(limits.max_value_recipients, 50);
assert!(limits.max_value_recipients > ParserLimits::default().max_value_recipients);
}
#[test]
fn test_value_recipients_limit_enforcement() {
let limits = ParserLimits::default();
assert!(
limits
.check_collection_size(19, limits.max_value_recipients, "value_recipients")
.is_ok()
);
assert!(
limits
.check_collection_size(20, limits.max_value_recipients, "value_recipients")
.is_err()
);
let result =
limits.check_collection_size(21, limits.max_value_recipients, "value_recipients");
assert!(result.is_err());
assert!(matches!(result, Err(LimitError::CollectionTooLarge { .. })));
}
}