sbom_walker/source/
file.rs1use crate::{
2 discover::DiscoveredSbom,
3 model::metadata::{self, SourceMetadata},
4 retrieve::RetrievedSbom,
5 source::Source,
6 visitors::store::DIR_METADATA,
7};
8use anyhow::{Context, anyhow};
9use bytes::Bytes;
10use std::{
11 fs,
12 io::ErrorKind,
13 path::{Path, PathBuf},
14 time::SystemTime,
15};
16use time::OffsetDateTime;
17use url::Url;
18use walker_common::{
19 retrieve::RetrievalMetadata,
20 source::file::{read_sig_and_digests, to_path},
21 utils::{self, openpgp::PublicKey},
22 validate::source::{Key, KeySource, KeySourceError},
23};
24
25#[non_exhaustive]
26#[derive(Clone, Debug, Default, PartialEq, Eq)]
27pub struct FileOptions {
28 pub since: Option<SystemTime>,
29}
30
31impl FileOptions {
32 pub fn new() -> Self {
33 Self::default()
34 }
35
36 pub fn since(mut self, since: impl Into<Option<SystemTime>>) -> Self {
37 self.since = since.into();
38 self
39 }
40}
41
42#[derive(Clone, Debug)]
44pub struct FileSource {
45 base: PathBuf,
47 options: FileOptions,
48}
49
50impl FileSource {
51 pub fn new(
52 base: impl AsRef<Path>,
53 options: impl Into<Option<FileOptions>>,
54 ) -> anyhow::Result<Self> {
55 Ok(Self {
56 base: fs::canonicalize(base)?,
57 options: options.into().unwrap_or_default(),
58 })
59 }
60
61 async fn scan_keys(&self) -> Result<Vec<metadata::Key>, anyhow::Error> {
62 let dir = self.base.join(DIR_METADATA).join("keys");
63
64 let mut result = Vec::new();
65
66 let mut entries = match tokio::fs::read_dir(&dir).await {
67 Err(err) if err.kind() == ErrorKind::NotFound => {
68 return Ok(result);
69 }
70 Err(err) => {
71 return Err(err)
72 .with_context(|| format!("Failed scanning for keys: {}", dir.display()));
73 }
74 Ok(entries) => entries,
75 };
76
77 while let Some(entry) = entries.next_entry().await? {
78 let path = entry.path();
79 if !path.is_file() {
80 continue;
81 }
82
83 #[allow(clippy::single_match)]
84 match path
85 .file_name()
86 .and_then(|s| s.to_str())
87 .and_then(|s| s.rsplit_once('.'))
88 {
89 Some((name, "txt")) => result.push(metadata::Key {
90 fingerprint: Some(name.to_string()),
91 url: Url::from_file_path(&path).map_err(|()| {
92 anyhow!("Failed to build file URL for: {}", path.display())
93 })?,
94 }),
95 Some((_, _)) | None => {}
96 }
97 }
98
99 Ok(result)
100 }
101}
102
103impl walker_common::source::Source for FileSource {
104 type Error = anyhow::Error;
105 type Retrieved = RetrievedSbom;
106}
107
108impl Source for FileSource {
109 async fn load_metadata(&self) -> Result<SourceMetadata, Self::Error> {
110 let metadata = self.base.join(DIR_METADATA).join("metadata.json");
111 let file = fs::File::open(&metadata)
112 .with_context(|| format!("Failed to open file: {}", metadata.display()))?;
113
114 let mut metadata: SourceMetadata =
115 serde_json::from_reader(&file).context("Failed to read stored provider metadata")?;
116
117 metadata.keys = self.scan_keys().await?;
118
119 Ok(metadata)
120 }
121
122 async fn load_index(&self) -> Result<Vec<DiscoveredSbom>, Self::Error> {
123 const SKIP: &[&str] = &[".asc", ".sha256", ".sha512"];
124
125 log::info!("Loading index - since: {:?}", self.options.since);
126
127 let mut entries = tokio::fs::read_dir(&self.base).await?;
128 let mut result = vec![];
129
130 'entry: while let Some(entry) = entries.next_entry().await? {
131 let path = entry.path();
132 if !path.is_file() {
133 continue;
134 }
135 let name = match path.file_name().and_then(|s| s.to_str()) {
136 Some(name) => name,
137 None => continue,
138 };
139
140 for ext in SKIP {
141 if name.ends_with(ext) {
142 log::debug!("Skipping file: {}", name);
143 continue 'entry;
144 }
145 }
146
147 if let Some(since) = self.options.since {
148 let modified = path.metadata()?.modified()?;
149 if modified < since {
150 log::debug!("Skipping file due to modification constraint: {modified:?}");
151 continue;
152 }
153 }
154
155 let url = Url::from_file_path(&path)
156 .map_err(|()| anyhow!("Failed to convert to URL: {}", path.display()))?;
157
158 let modified = path.metadata()?.modified()?;
159
160 result.push(DiscoveredSbom { url, modified })
161 }
162
163 Ok(result)
164 }
165
166 async fn load_sbom(&self, discovered: DiscoveredSbom) -> Result<RetrievedSbom, Self::Error> {
167 let path = discovered
168 .url
169 .to_file_path()
170 .map_err(|()| anyhow!("Unable to convert URL into path: {}", discovered.url))?;
171
172 let data = Bytes::from(tokio::fs::read(&path).await?);
173
174 let (signature, sha256, sha512) = read_sig_and_digests(&path, &data).await?;
175
176 let last_modification = path
177 .metadata()
178 .ok()
179 .and_then(|md| md.modified().ok())
180 .map(OffsetDateTime::from);
181
182 Ok(RetrievedSbom {
183 discovered,
184 data,
185 signature,
186 sha256,
187 sha512,
188 metadata: RetrievalMetadata {
189 last_modification,
190 etag: None,
191 },
192 })
193 }
194}
195
196impl KeySource for FileSource {
197 type Error = anyhow::Error;
198
199 async fn load_public_key(
200 &self,
201 key: Key<'_>,
202 ) -> Result<PublicKey, KeySourceError<Self::Error>> {
203 let bytes = tokio::fs::read(to_path(key.url).map_err(KeySourceError::Source)?)
204 .await
205 .map_err(|err| KeySourceError::Source(err.into()))?;
206 utils::openpgp::validate_keys(bytes.into(), key.fingerprint)
207 .map_err(KeySourceError::OpenPgp)
208 }
209}