csaf_walker/visitors/skip/
mod.rs

1use crate::discover::{DiscoveredAdvisory, DiscoveredContext, DiscoveredVisitor};
2use crate::model::store::distribution_base;
3use crate::source::Source;
4use crate::validation::{ValidatedAdvisory, ValidatedVisitor, ValidationContext, ValidationError};
5use std::fmt::{Debug, Display};
6use std::path::PathBuf;
7use std::time::SystemTime;
8use tokio::fs;
9use walker_common::utils::url::Urlify;
10
11#[derive(Debug, thiserror::Error)]
12pub enum Error<VE: Display + Debug> {
13    #[error("{0}")]
14    Visitor(VE),
15    #[error("I/O error: {0}")]
16    Io(#[from] std::io::Error),
17    #[error("Unable to get name from URL")]
18    Name,
19}
20
21/// A visitor, skipping advisories for existing files.
22pub struct SkipExistingVisitor<V: DiscoveredVisitor> {
23    pub visitor: V,
24    pub output: PathBuf,
25    /// The time "since" when we consider changes "new"
26    ///
27    /// Overrides the "file modified" timestamp which is used by default.
28    pub since: Option<SystemTime>,
29}
30
31impl<V: DiscoveredVisitor> DiscoveredVisitor for SkipExistingVisitor<V> {
32    type Error = Error<V::Error>;
33    type Context = V::Context;
34
35    async fn visit_context(
36        &self,
37        context: &DiscoveredContext<'_>,
38    ) -> Result<Self::Context, Self::Error> {
39        self.visitor
40            .visit_context(context)
41            .await
42            .map_err(Error::Visitor)
43    }
44
45    async fn visit_advisory(
46        &self,
47        context: &Self::Context,
48        advisory: DiscoveredAdvisory,
49    ) -> Result<(), Self::Error> {
50        let name = match advisory.context.url().clone().make_relative(&advisory.url) {
51            Some(name) => name,
52            None => return Err(Error::Name),
53        };
54        let path = distribution_base(&self.output, advisory.context.url().as_str()).join(&name);
55
56        if fs::try_exists(&path).await? {
57            // if we have a "since", we use it as the file modification timestamp
58            let file_modified = match self.since {
59                Some(since) => since,
60                None => fs::metadata(&path).await?.modified()?,
61            };
62
63            log::debug!(
64                "Advisory modified: {}, file ({}) modified: {} ({:?})",
65                humantime::Timestamp::from(advisory.modified),
66                name,
67                humantime::Timestamp::from(file_modified),
68                self.since.map(humantime::Timestamp::from)
69            );
70
71            if file_modified >= advisory.modified {
72                // the file was modified after the change date, skip it
73                return Ok(());
74            }
75        } else {
76            log::debug!("File did not exist: {}", path.display());
77        }
78
79        self.visitor
80            .visit_advisory(context, advisory)
81            .await
82            .map_err(Error::Visitor)
83    }
84}
85
86/// A visitor which will skip (with a warning) any failed document.
87pub struct SkipFailedVisitor<V> {
88    pub visitor: V,
89    pub skip_failures: bool,
90}
91
92impl<V> SkipFailedVisitor<V> {
93    pub fn new(visitor: V) -> Self {
94        Self {
95            visitor,
96            skip_failures: true,
97        }
98    }
99}
100
101impl<V: ValidatedVisitor<S>, S: Source> ValidatedVisitor<S> for SkipFailedVisitor<V> {
102    type Error = V::Error;
103    type Context = V::Context;
104
105    async fn visit_context(
106        &self,
107        context: &ValidationContext<'_>,
108    ) -> Result<Self::Context, Self::Error> {
109        self.visitor.visit_context(context).await
110    }
111
112    async fn visit_advisory(
113        &self,
114        context: &Self::Context,
115        result: Result<ValidatedAdvisory, ValidationError<S>>,
116    ) -> Result<(), Self::Error> {
117        match (self.skip_failures, result) {
118            (_, Ok(result)) => self.visitor.visit_advisory(context, Ok(result)).await,
119            (false, Err(err)) => self.visitor.visit_advisory(context, Err(err)).await,
120            (true, Err(err)) => {
121                log::warn!("Skipping failed advisory ({}): {err}", err.url());
122                Ok(())
123            }
124        }
125    }
126}