1use crate::{
2 discover::DiscoveredSbom,
3 model::metadata::SourceMetadata,
4 retrieve::{RetrievalContext, RetrievedSbom, RetrievedVisitor},
5 source::Source,
6 validation::{ValidatedSbom, ValidatedVisitor, ValidationContext},
7};
8use anyhow::Context;
9use sequoia_openpgp::{Cert, armor::Kind, serialize::SerializeInto};
10use std::{
11 io::{ErrorKind, Write},
12 path::{Path, PathBuf},
13};
14use tokio::fs;
15use walker_common::{
16 retrieve::RetrievalError,
17 store::{Document, StoreError, store_document},
18 utils::openpgp::PublicKey,
19 validate::ValidationError,
20};
21
22pub const DIR_METADATA: &str = "metadata";
23
24#[non_exhaustive]
26pub struct StoreVisitor {
27 pub base: PathBuf,
29
30 pub no_timestamps: bool,
32
33 #[cfg(any(target_os = "linux", target_os = "macos"))]
35 pub no_xattrs: bool,
36}
37
38impl StoreVisitor {
39 pub fn new(base: impl Into<PathBuf>) -> Self {
40 Self {
41 base: base.into(),
42 no_timestamps: false,
43 #[cfg(any(target_os = "linux", target_os = "macos"))]
44 no_xattrs: false,
45 }
46 }
47
48 pub fn no_timestamps(mut self, no_timestamps: bool) -> Self {
49 self.no_timestamps = no_timestamps;
50 self
51 }
52
53 #[cfg(any(target_os = "linux", target_os = "macos"))]
54 pub fn no_xattrs(mut self, no_xattrs: bool) -> Self {
55 self.no_xattrs = no_xattrs;
56 self
57 }
58}
59
60#[derive(Debug, thiserror::Error)]
61pub enum StoreRetrievedError<S: Source> {
62 #[error(transparent)]
63 Store(#[from] StoreError),
64 #[error(transparent)]
65 Retrieval(#[from] RetrievalError<DiscoveredSbom, S>),
66}
67
68#[derive(Debug, thiserror::Error)]
69pub enum StoreValidatedError<S: Source> {
70 #[error(transparent)]
71 Store(#[from] StoreError),
72 #[error(transparent)]
73 Validation(#[from] ValidationError<S>),
74}
75
76impl<S: Source> RetrievedVisitor<S> for StoreVisitor {
77 type Error = StoreRetrievedError<S>;
78 type Context = ();
79
80 async fn visit_context(
81 &self,
82 context: &RetrievalContext<'_>,
83 ) -> Result<Self::Context, Self::Error> {
84 self.store_provider_metadata(context.metadata).await?;
85 self.store_keys(context.keys).await?;
86 Ok(())
87 }
88
89 async fn visit_sbom(
90 &self,
91 _context: &Self::Context,
92 result: Result<RetrievedSbom, RetrievalError<DiscoveredSbom, S>>,
93 ) -> Result<(), Self::Error> {
94 self.store(&result?).await?;
95 Ok(())
96 }
97}
98
99impl<S: Source> ValidatedVisitor<S> for StoreVisitor {
100 type Error = StoreValidatedError<S>;
101 type Context = ();
102
103 async fn visit_context(
104 &self,
105 context: &ValidationContext<'_>,
106 ) -> Result<Self::Context, Self::Error> {
107 self.store_provider_metadata(context.metadata).await?;
108 self.store_keys(context.retrieval.keys).await?;
109 Ok(())
110 }
111
112 async fn visit_sbom(
113 &self,
114 _context: &Self::Context,
115 result: Result<ValidatedSbom, ValidationError<S>>,
116 ) -> Result<(), Self::Error> {
117 self.store(&result?.retrieved).await?;
118 Ok(())
119 }
120}
121
122impl StoreVisitor {
123 async fn store_provider_metadata(&self, metadata: &SourceMetadata) -> Result<(), StoreError> {
124 let metadir = self.base.join(DIR_METADATA);
125
126 fs::create_dir(&metadir)
127 .await
128 .or_else(|err| match err.kind() {
129 ErrorKind::AlreadyExists => Ok(()),
130 _ => Err(err),
131 })
132 .with_context(|| format!("Failed to create metadata directory: {}", metadir.display()))
133 .map_err(StoreError::Io)?;
134
135 let file = metadir.join("metadata.json");
136 let mut out = std::fs::File::create(&file)
137 .with_context(|| {
138 format!(
139 "Unable to open provider metadata file for writing: {}",
140 file.display()
141 )
142 })
143 .map_err(StoreError::Io)?;
144 serde_json::to_writer_pretty(&mut out, metadata)
145 .context("Failed serializing provider metadata")
146 .map_err(StoreError::Io)?;
147 Ok(())
148 }
149
150 async fn store_keys(&self, keys: &[PublicKey]) -> Result<(), StoreError> {
151 let metadata = self.base.join(DIR_METADATA).join("keys");
152 fs::create_dir(&metadata)
153 .await
154 .or_else(|err| match err.kind() {
156 ErrorKind::AlreadyExists => Ok(()),
157 _ => Err(err),
158 })
159 .with_context(|| {
160 format!(
161 "Failed to create metadata directory: {}",
162 metadata.display()
163 )
164 })
165 .map_err(StoreError::Io)?;
166
167 for cert in keys.iter().flat_map(|k| &k.certs) {
168 log::info!("Storing key: {}", cert.fingerprint());
169 self.store_cert(cert, &metadata).await?;
170 }
171
172 Ok(())
173 }
174
175 async fn store_cert(&self, cert: &Cert, path: &Path) -> Result<(), StoreError> {
176 let name = path.join(format!("{}.txt", cert.fingerprint().to_hex()));
177
178 let data = Self::serialize_key(cert).map_err(StoreError::SerializeKey)?;
179
180 fs::write(&name, data)
181 .await
182 .with_context(|| format!("Failed to store key: {}", name.display()))
183 .map_err(StoreError::Io)?;
184 Ok(())
185 }
186
187 fn serialize_key(cert: &Cert) -> Result<Vec<u8>, anyhow::Error> {
188 let mut writer = sequoia_openpgp::armor::Writer::new(Vec::new(), Kind::PublicKey)?;
189 writer.write_all(&cert.to_vec()?)?;
190 Ok(writer.finalize()?)
191 }
192
193 async fn store(&self, sbom: &RetrievedSbom) -> Result<(), StoreError> {
194 log::info!(
195 "Storing: {} (modified: {:?})",
196 sbom.url,
197 sbom.metadata.last_modification
198 );
199
200 let file = PathBuf::from(sbom.url.path())
201 .file_name()
202 .map(|file| self.base.join(file))
203 .ok_or_else(|| StoreError::Filename(sbom.url.to_string()))?;
204
205 log::debug!("Writing {}", file.display());
206
207 store_document(
208 &file,
209 Document {
210 data: &sbom.data,
211 changed: sbom.modified,
212 metadata: &sbom.metadata,
213 sha256: &sbom.sha256,
214 sha512: &sbom.sha512,
215 signature: &sbom.signature,
216 no_timestamps: self.no_timestamps,
217 #[cfg(any(target_os = "linux", target_os = "macos"))]
218 no_xattrs: self.no_xattrs,
219 },
220 )
221 .await?;
222
223 Ok(())
224 }
225}