use super::{ComplianceRequirement, RequirementCategory, RequirementLevel, ValidationError};
use regex::Regex;
use std::fs;
use std::path::Path;
pub struct RfcParser {
must_pattern: Regex,
must_not_pattern: Regex,
should_pattern: Regex,
should_not_pattern: Regex,
may_pattern: Regex,
}
impl Default for RfcParser {
fn default() -> Self {
Self::new()
}
}
impl RfcParser {
#[allow(clippy::expect_used)]
pub fn new() -> Self {
Self {
must_pattern: Regex::new(r"\b(MUST|SHALL|REQUIRED)\b")
.expect("Static regex pattern should always compile"),
must_not_pattern: Regex::new(r"\b(MUST NOT|SHALL NOT)\b")
.expect("Static regex pattern should always compile"),
should_pattern: Regex::new(r"\b(SHOULD|RECOMMENDED)\b")
.expect("Static regex pattern should always compile"),
should_not_pattern: Regex::new(r"\b(SHOULD NOT|NOT RECOMMENDED)\b")
.expect("Static regex pattern should always compile"),
may_pattern: Regex::new(r"\b(MAY|OPTIONAL)\b")
.expect("Static regex pattern should always compile"),
}
}
pub fn parse_file(&self, path: &Path) -> Result<Vec<ComplianceRequirement>, ValidationError> {
let content = fs::read_to_string(path)?;
let spec_id = self.extract_spec_id(path)?;
Ok(self.parse_content(&content, &spec_id))
}
pub fn parse_content(&self, content: &str, spec_id: &str) -> Vec<ComplianceRequirement> {
let mut requirements = Vec::new();
let sections = self.split_into_sections(content);
for (section_num, section_content) in sections {
let section_reqs =
self.extract_requirements_from_section(spec_id, §ion_num, §ion_content);
requirements.extend(section_reqs);
}
requirements
}
#[allow(clippy::expect_used)]
fn split_into_sections(&self, content: &str) -> Vec<(String, String)> {
let mut sections = Vec::new();
let section_regex = Regex::new(r"(?m)^(\d+(?:\.\d+)*)\s+(.+)$")
.expect("Static regex pattern should always compile");
let mut current_section = String::new();
let mut current_content = String::new();
for line in content.lines() {
if let Some(captures) = section_regex.captures(line) {
if !current_section.is_empty() {
sections.push((current_section.clone(), current_content.clone()));
}
current_section = captures[1].to_string();
current_content = String::new();
} else {
current_content.push_str(line);
current_content.push('\n');
}
}
if !current_section.is_empty() {
sections.push((current_section, current_content));
}
sections
}
fn extract_requirements_from_section(
&self,
spec_id: &str,
section: &str,
content: &str,
) -> Vec<ComplianceRequirement> {
let mut requirements = Vec::new();
let sentences = self.split_into_sentences(content);
for sentence in sentences {
if let Some(req) = self.extract_requirement_from_sentence(spec_id, section, &sentence) {
requirements.push(req);
}
}
requirements
}
#[allow(clippy::expect_used)]
fn split_into_sentences(&self, text: &str) -> Vec<String> {
let sentence_regex =
Regex::new(r"[.!?]+\s+").expect("Static regex pattern should always compile");
sentence_regex
.split(text)
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
}
fn extract_requirement_from_sentence(
&self,
spec_id: &str,
section: &str,
sentence: &str,
) -> Option<ComplianceRequirement> {
let level = if self.must_not_pattern.is_match(sentence) {
RequirementLevel::MustNot
} else if self.should_not_pattern.is_match(sentence) {
RequirementLevel::ShouldNot
} else if self.must_pattern.is_match(sentence) {
RequirementLevel::Must
} else if self.should_pattern.is_match(sentence) {
RequirementLevel::Should
} else if self.may_pattern.is_match(sentence) {
RequirementLevel::May
} else {
return None;
};
let category = self.categorize_requirement(sentence);
Some(ComplianceRequirement {
spec_id: spec_id.to_string(),
section: section.to_string(),
level,
description: sentence.to_string(),
category,
})
}
fn categorize_requirement(&self, description: &str) -> RequirementCategory {
let lower = description.to_lowercase();
if lower.contains("transport parameter") || lower.contains("transport_parameter") {
RequirementCategory::TransportParameters
} else if lower.contains("frame")
|| lower.contains("encoding")
|| lower.contains("decoding")
{
RequirementCategory::FrameFormat
} else if lower.contains("nat")
|| lower.contains("traversal")
|| lower.contains("hole punch")
{
RequirementCategory::NatTraversal
} else if lower.contains("address") && lower.contains("discovery") {
RequirementCategory::AddressDiscovery
} else if lower.contains("error") || lower.contains("close") || lower.contains("reset") {
RequirementCategory::ErrorHandling
} else if lower.contains("crypto")
|| lower.contains("security")
|| lower.contains("authentication")
{
RequirementCategory::Security
} else if lower.contains("connection")
|| lower.contains("handshake")
|| lower.contains("establishment")
{
RequirementCategory::ConnectionEstablishment
} else if lower.contains("performance")
|| lower.contains("throughput")
|| lower.contains("latency")
{
RequirementCategory::Performance
} else {
RequirementCategory::Transport
}
}
fn extract_spec_id(&self, path: &Path) -> Result<String, ValidationError> {
let filename = path
.file_stem()
.and_then(|s| s.to_str())
.ok_or_else(|| ValidationError::RfcParseError("Invalid file path".to_string()))?;
if filename.starts_with("rfc") {
Ok(filename.to_uppercase())
} else if filename.contains("draft") {
Ok(filename.to_string())
} else {
Ok(format!("spec-{filename}"))
}
}
}
pub struct QuicRfcParser {
parser: RfcParser,
}
impl Default for QuicRfcParser {
fn default() -> Self {
Self::new()
}
}
impl QuicRfcParser {
pub fn new() -> Self {
Self {
parser: RfcParser::new(),
}
}
pub fn parse_rfc9000(&self, content: &str) -> Vec<ComplianceRequirement> {
let mut requirements = self.parser.parse_content(content, "RFC9000");
self.add_rfc9000_specific_requirements(&mut requirements);
requirements
}
pub fn parse_address_discovery_draft(&self, content: &str) -> Vec<ComplianceRequirement> {
let mut requirements = self
.parser
.parse_content(content, "draft-ietf-quic-address-discovery-00");
self.add_address_discovery_requirements(&mut requirements);
requirements
}
pub fn parse_nat_traversal_draft(&self, content: &str) -> Vec<ComplianceRequirement> {
let mut requirements = self
.parser
.parse_content(content, "draft-seemann-quic-nat-traversal-02");
self.add_nat_traversal_requirements(&mut requirements);
requirements
}
fn add_rfc9000_specific_requirements(&self, requirements: &mut Vec<ComplianceRequirement>) {
requirements.push(ComplianceRequirement {
spec_id: "RFC9000".to_string(),
section: "4.1".to_string(),
level: RequirementLevel::Must,
description: "Endpoints MUST validate transport parameters during handshake"
.to_string(),
category: RequirementCategory::TransportParameters,
});
requirements.push(ComplianceRequirement {
spec_id: "RFC9000".to_string(),
section: "12.4".to_string(),
level: RequirementLevel::Must,
description:
"An endpoint MUST NOT send data on a stream without available flow control credit"
.to_string(),
category: RequirementCategory::Transport,
});
}
fn add_address_discovery_requirements(&self, requirements: &mut Vec<ComplianceRequirement>) {
requirements.push(ComplianceRequirement {
spec_id: "draft-ietf-quic-address-discovery-00".to_string(),
section: "3.1".to_string(),
level: RequirementLevel::Must,
description:
"OBSERVED_ADDRESS frames MUST include monotonically increasing sequence numbers"
.to_string(),
category: RequirementCategory::AddressDiscovery,
});
requirements.push(ComplianceRequirement {
spec_id: "draft-ietf-quic-address-discovery-00".to_string(),
section: "3.2".to_string(),
level: RequirementLevel::Must,
description:
"The IP version MUST be determined by the least significant bit of the frame type"
.to_string(),
category: RequirementCategory::AddressDiscovery,
});
}
fn add_nat_traversal_requirements(&self, requirements: &mut Vec<ComplianceRequirement>) {
requirements.push(ComplianceRequirement {
spec_id: "draft-seemann-quic-nat-traversal-02".to_string(),
section: "4.1".to_string(),
level: RequirementLevel::Must,
description: "Clients MUST send empty NAT traversal transport parameter".to_string(),
category: RequirementCategory::NatTraversal,
});
requirements.push(ComplianceRequirement {
spec_id: "draft-seemann-quic-nat-traversal-02".to_string(),
section: "4.1".to_string(),
level: RequirementLevel::Must,
description: "Servers MUST send concurrency limit in NAT traversal transport parameter"
.to_string(),
category: RequirementCategory::NatTraversal,
});
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rfc_parser_creation() {
let parser = RfcParser::new();
assert!(parser.must_pattern.is_match("MUST implement"));
assert!(parser.must_not_pattern.is_match("MUST NOT send"));
assert!(parser.should_pattern.is_match("SHOULD use"));
assert!(parser.should_not_pattern.is_match("SHOULD NOT ignore"));
assert!(parser.may_pattern.is_match("MAY include"));
}
#[test]
fn test_requirement_extraction() {
let parser = RfcParser::new();
let sentence = "Endpoints MUST validate all received transport parameters.";
let req = parser.extract_requirement_from_sentence("RFC9000", "4.1", sentence);
assert!(req.is_some());
let req = req.unwrap();
assert_eq!(req.level, RequirementLevel::Must);
assert_eq!(req.category, RequirementCategory::TransportParameters);
}
#[test]
fn test_categorization() {
let parser = RfcParser::new();
assert_eq!(
parser.categorize_requirement("transport parameter validation"),
RequirementCategory::TransportParameters
);
assert_eq!(
parser.categorize_requirement("frame encoding rules"),
RequirementCategory::FrameFormat
);
assert_eq!(
parser.categorize_requirement("NAT traversal mechanism"),
RequirementCategory::NatTraversal
);
}
#[test]
fn test_sentence_splitting() {
let parser = RfcParser::new();
let text = "This is sentence one. This is sentence two! And sentence three?";
let sentences = parser.split_into_sentences(text);
assert_eq!(sentences.len(), 3);
assert_eq!(sentences[0], "This is sentence one");
assert_eq!(sentences[1], "This is sentence two");
assert_eq!(sentences[2], "And sentence three?");
}
#[test]
fn test_quic_rfc_parser() {
let parser = QuicRfcParser::new();
let content = "Endpoints MUST validate parameters. They SHOULD log errors.";
let requirements = parser.parse_rfc9000(content);
assert!(requirements.len() >= 2); }
}