sbom_walker/visitors/skip/
mod.rs

1use crate::{
2    discover::{DiscoveredContext, DiscoveredSbom, DiscoveredVisitor},
3    source::Source,
4    validation::{ValidatedSbom, ValidatedVisitor, ValidationContext},
5};
6use std::{
7    fmt::{Debug, Display},
8    path::PathBuf,
9    time::SystemTime,
10};
11use tokio::fs;
12use walker_common::{utils::url::Urlify, validate::ValidationError};
13
14#[derive(Debug, thiserror::Error)]
15pub enum Error<VE: Display + Debug> {
16    #[error("{0}")]
17    Visitor(VE),
18    #[error("I/O error: {0}")]
19    Io(#[from] std::io::Error),
20    #[error("Unable to get name from URL")]
21    Name,
22}
23
24/// A visitor, skipping advisories for existing files.
25pub struct SkipExistingVisitor<V: DiscoveredVisitor> {
26    pub visitor: V,
27    pub output: PathBuf,
28    /// The time "since" when we consider changes "new"
29    ///
30    /// Overrides the "file modified" timestamp which is used by default.
31    pub since: Option<SystemTime>,
32}
33
34impl<V: DiscoveredVisitor> DiscoveredVisitor for SkipExistingVisitor<V> {
35    type Error = Error<V::Error>;
36    type Context = V::Context;
37
38    async fn visit_context(
39        &self,
40        context: &DiscoveredContext<'_>,
41    ) -> Result<Self::Context, Self::Error> {
42        self.visitor
43            .visit_context(context)
44            .await
45            .map_err(Error::Visitor)
46    }
47
48    async fn visit_sbom(
49        &self,
50        context: &Self::Context,
51        sbom: DiscoveredSbom,
52    ) -> Result<(), Self::Error> {
53        let name = PathBuf::from(sbom.url.path());
54        let name = name.file_name().ok_or(Error::Name)?;
55
56        let path = self.output.join(name);
57
58        if fs::try_exists(&path).await? {
59            // if we have a "since", we use it as the file modification timestamp
60            let file_modified = match self.since {
61                Some(since) => since,
62                None => fs::metadata(&path).await?.modified()?,
63            };
64
65            log::debug!(
66                "Advisory modified: {}, file ({}) modified: {} ({:?})",
67                humantime::Timestamp::from(sbom.modified),
68                name.to_string_lossy(),
69                humantime::Timestamp::from(file_modified),
70                self.since.map(humantime::Timestamp::from)
71            );
72
73            if file_modified >= sbom.modified {
74                // the file was modified after the change date, skip it
75                return Ok(());
76            }
77        }
78
79        self.visitor
80            .visit_sbom(context, sbom)
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_sbom(
113        &self,
114        context: &Self::Context,
115        result: Result<ValidatedSbom, ValidationError<S>>,
116    ) -> Result<(), Self::Error> {
117        match (self.skip_failures, result) {
118            (_, Ok(result)) => self.visitor.visit_sbom(context, Ok(result)).await,
119            (false, Err(err)) => self.visitor.visit_sbom(context, Err(err)).await,
120            (true, Err(err)) => {
121                log::warn!("Skipping failed SBOM {}: {err}", err.url());
122                Ok(())
123            }
124        }
125    }
126}