Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
68 changes: 68 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions csaf-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ serde_json = { version = "1", features = ["preserve_order"] }

[dev-dependencies]
rstest = "0.26.1"
rstest_reuse = "0.7.0"
criterion = { version = "0.8", features = ["html_reports"] }
tempfile = "3"

Expand Down
1 change: 1 addition & 0 deletions csaf-rs/src/validations/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,5 +137,6 @@ pub mod test_6_3_11;
pub mod test_6_3_12;
pub mod test_6_3_14;
pub mod test_6_3_15;
pub mod test_6_3_16;
pub mod test_6_3_18;
pub mod test_6_3_20;
241 changes: 241 additions & 0 deletions csaf-rs/src/validations/test_6_3_16.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
use crate::csaf::types::language::{CsafLanguage, ValidCsafLanguage};
use crate::csaf_traits::{
AcknowledgmentTrait, AggregateSeverityTrait, CsafTrait, CsafVersion, DistributionTrait, DocumentTrait,
InvolvementTrait, NoteTrait, ProductGroupTrait, ProductTreeTrait, PublisherTrait, ReferenceTrait, RemediationTrait,
RestartRequiredTrait, RevisionTrait, ThreatTrait, TrackingTrait, VulnerabilityTrait,
};
use crate::validation::{TestFinding, TestFindingData};
use crate::validations::utils::text_check::{TextCheckKind, TextChecker, TextCheckerMatchingError, select_checker};

fn create_grammar_mistake_finding_info(
fragment: &str,
start: usize,
end: usize,
replacement: &Option<String>,
instance_path: &str,
) -> TestFinding {
let fix = replacement
.as_deref()
.map(|r| format!(", suggested fix: `{r}`"))
.unwrap_or_default();
TestFinding::Information(TestFindingData {
message: format!("Grammar mistake: `{fragment}` (position {start}-{end}{fix})"),
instance_path: instance_path.to_string(),
})
}

/// 6.3.16 Grammar Check
///
/// If the document language is given it MUST be tested that a grammar check for the given
/// language does not find any mistakes. The test is skipped if the document language is not
/// set. It fails if the given language is not supported (only English is currently supported).
pub fn test_6_3_16_grammar_check(doc: &impl CsafTrait) -> Result<(), Vec<TestFinding>> {
// Select the checker once for this lang/kind
// Note: If this is run as a unit test, the matcher will return a mock
test_6_3_16_grammar_check_impl(doc, |lang| select_checker(TextCheckKind::Grammar, lang))
}

/// Shared implementation, used by production code
/// (with auto-selected checker) and integration tests (with a single, fixed checker, see text_check/integration_tests).
pub fn test_6_3_16_grammar_check_impl(
doc: &impl CsafTrait,
select_checker: impl FnOnce(&ValidCsafLanguage) -> Result<Box<dyn TextChecker>, TextCheckerMatchingError>,
) -> Result<(), Vec<TestFinding>> {
let document = doc.get_document();

// Skip this test if language is not set
let lang = match document.get_lang() {
None => return Ok(()), // #409 skipped
Some(lang) => lang,
};

// Check if the language is supported
let lang = match &lang {
CsafLanguage::Valid(valid_lang) => valid_lang,
_ => {
return Err(vec![TestFinding::Information(TestFindingData {
message: format!("Grammar check does not support language '{lang}'"),
instance_path: "/document/lang".to_string(),
})]);
},
};

let mut errors: Option<Vec<TestFinding>> = None;

// Select the checker once for this lang/kind
// Note: If this is run as a unit test, the matcher will return a mock
let checker = select_checker(lang).map_err(|err| vec![TestFinding::Information(err.into())])?;

// Runs the grammar-check for a single piece of text and appends any resulting findings
let mut check = |text: &str, instance_path: String| {
for finding in checker.check_text(TextCheckKind::Grammar, text) {
errors.get_or_insert_default().push(create_grammar_mistake_finding_info(
&finding.fragment,
finding.start,
finding.end,
&finding.replacement,
&instance_path,
));
}
};

// Check all text fields listed in the spec
if let Some(acknowledgments) = document.get_acknowledgments() {
for (a_i, ack) in acknowledgments.iter().enumerate() {
if let Some(summary) = ack.get_summary() {
check(summary, format!("/document/acknowledgments/{a_i}/summary"));
}
}
}

if let Some(aggregate_severity) = document.get_aggregate_severity() {
check(
aggregate_severity.get_text(),
"/document/aggregate_severity/text".to_string(),
);
}

let distribution_text = match document.get_csaf_version() {
CsafVersion::X20 => document.get_distribution_20().and_then(|d| d.get_text()),
CsafVersion::X21 => document.get_distribution_21().ok().and_then(|d| d.get_text()),
};
if let Some(text) = distribution_text {
check(text, "/document/distribution/text".to_string());
}

if let Some(notes) = document.get_notes() {
for (n_i, note) in notes.iter().enumerate() {
if let Some(audience) = note.get_audience() {
check(audience, format!("/document/notes/{n_i}/audience"));
}
check(note.get_text(), format!("/document/notes/{n_i}/text"));
if let Some(title) = note.get_title() {
check(title, format!("/document/notes/{n_i}/title"));
}
}
}

let publisher = document.get_publisher();
if let Some(issuing_authority) = publisher.get_issuing_authority() {
check(issuing_authority, "/document/publisher/issuing_authority".to_string());
}

if let Some(references) = document.get_references() {
for (r_i, reference) in references.iter().enumerate() {
check(reference.get_summary(), format!("/document/references/{r_i}/summary"));
}
}

check(document.get_title(), "/document/title".to_string());

let tracking = document.get_tracking();
for (r_i, revision) in tracking.get_revision_history().iter().enumerate() {
check(
revision.get_summary(),
format!("/document/tracking/revision_history/{r_i}/summary"),
);
}

if let Some(product_tree) = doc.get_product_tree() {
for (pg_i, product_group) in product_tree.get_product_groups().iter().enumerate() {
if let Some(summary) = product_group.get_summary() {
check(summary, format!("/product_tree/product_groups/{pg_i}/summary"));
}
}
}

for (v_i, vuln) in doc.get_vulnerabilities().iter().enumerate() {
let vuln_prefix = format!("/vulnerabilities/{v_i}");

if let Some(acknowledgments) = vuln.get_acknowledgments() {
for (a_i, ack) in acknowledgments.iter().enumerate() {
if let Some(summary) = ack.get_summary() {
check(summary, format!("{vuln_prefix}/acknowledgments/{a_i}/summary"));
}
}
}

for (i_i, involvement) in vuln.get_involvements().iter().flat_map(|v| v.iter()).enumerate() {
if let Some(summary) = involvement.get_summary() {
check(summary, format!("{vuln_prefix}/involvements/{i_i}/summary"));
}
}

if let Some(notes) = vuln.get_notes() {
for (n_i, note) in notes.iter().enumerate() {
if let Some(audience) = note.get_audience() {
check(audience, format!("{vuln_prefix}/notes/{n_i}/audience"));
}
check(note.get_text(), format!("{vuln_prefix}/notes/{n_i}/text"));
if let Some(title) = note.get_title() {
check(title, format!("{vuln_prefix}/notes/{n_i}/title"));
}
}
}

if let Some(references) = vuln.get_references() {
for (r_i, reference) in references.iter().enumerate() {
check(
reference.get_summary(),
format!("{vuln_prefix}/references/{r_i}/summary"),
);
}
}

for (r_i, remediation) in vuln.get_remediations().iter().enumerate() {
check(
remediation.get_details(),
format!("{vuln_prefix}/remediations/{r_i}/details"),
);
for (e_i, entitlement) in remediation.get_entitlements().into_iter().enumerate() {
check(
entitlement,
format!("{vuln_prefix}/remediations/{r_i}/entitlements/{e_i}"),
);
}
if let Some(restart_required) = remediation.get_restart_required()
&& let Some(details) = restart_required.get_details()
{
check(
details,
format!("{vuln_prefix}/remediations/{r_i}/restart_required/details"),
);
}
}

for (t_i, threat) in vuln.get_threats().iter().enumerate() {
check(threat.get_details(), format!("{vuln_prefix}/threats/{t_i}/details"));
}

if let Some(title) = vuln.get_title() {
check(title, format!("{vuln_prefix}/title"));
}
}

errors.map_or(Ok(()), Err)
}

crate::test_validation::impl_validator!(csaf2_1, ValidatorForTest6_3_16, test_6_3_16_grammar_check);

#[cfg(test)]
/// Expected results, also used by the text_check/integration tests
pub(crate) static EXPECTED_RESULTS_2_1: std::sync::LazyLock<crate::csaf2_1::testcases::ExpectedResults_6_3_16> =
std::sync::LazyLock::new(|| crate::csaf2_1::testcases::ExpectedResults_6_3_16 {
case_01: Err(vec![
create_grammar_mistake_finding_info("must followed", 29, 42, &None, "/document/notes/0/text"),
create_grammar_mistake_finding_info("for ensure", 43, 53, &None, "/document/notes/0/text"),
create_grammar_mistake_finding_info("a products", 75, 85, &None, "/document/notes/0/text"),
]),
case_11: Ok(()),
});

#[cfg(test)]
mod tests {
use super::*;
use crate::csaf2_1::testcases::TESTS_2_1;

#[test]
fn test_test_6_3_16() {
TESTS_2_1.test_6_3_16.expect(EXPECTED_RESULTS_2_1.to_owned());
}
}
Loading
Loading