1use crate::layer::LayerId;
12
13fn discovery_matches(bytes: &[u8], layer: &LayerRef) -> bool {
18 use crate::manifest::LayerManifest;
19 let candidate_payloads = || -> Vec<Vec<u8>> {
20 let mut out = vec![bytes.to_vec()];
21 if let Ok(text) = std::str::from_utf8(bytes)
22 && let Ok(env) = wsc::dsse::DsseEnvelope::from_json(text)
23 && let Ok(payload) = env.payload_bytes()
24 {
25 out.push(payload);
26 }
27 out
28 };
29 match layer {
30 LayerRef::Digest(digest) => candidate_payloads()
31 .iter()
32 .any(|p| &crate::store::manifest_digest(p) == digest),
33 LayerRef::Name(id) => candidate_payloads()
34 .iter()
35 .any(|p| LayerManifest::parse(p).is_ok_and(|m| &m.layer == id)),
36 }
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum LayerRef {
42 Name(LayerId),
45 Digest(String),
47}
48
49#[derive(Debug, thiserror::Error)]
53pub enum SourceError {
54 #[error("source has no layer matching {0}")]
55 NotFound(String),
56 #[error(
62 "this archive carries no payload for {wanted} — it was archived for {archived_for}, and \
63 `varve archive` exports only the payloads the archiving machine installed, so it holds \
64 {archived_for} payloads and nothing else (blob {digest} is not in it). Install the layer \
65 on a machine running {wanted} and archive it there to carry {wanted} across the gap."
66 )]
67 NoPayloadForPlatform {
68 digest: String,
69 wanted: String,
70 archived_for: String,
71 },
72 #[error("source transport error: {0}")]
73 Transport(String),
74}
75
76pub trait LayerSource {
80 fn fetch_manifest(&self, layer: &LayerRef) -> Result<Vec<u8>, SourceError>;
82 fn fetch_blob(&self, digest: &str) -> Result<Vec<u8>, SourceError>;
84 fn fetch_line_status(&self, _layer: &LayerRef) -> Result<Option<Vec<u8>>, SourceError> {
91 Ok(None)
92 }
93
94 fn fetch_line_index(&self, _line: &str) -> Result<Option<Vec<u8>>, SourceError> {
100 Ok(None)
101 }
102
103 fn fetch_attestations(
114 &self,
115 _layer: &LayerRef,
116 ) -> Result<Vec<crate::attestcarry::CarriedAttestation>, SourceError> {
117 Ok(Vec::new())
118 }
119
120 fn served_layers(&self, _line: &str) -> Result<Option<Vec<String>>, SourceError> {
126 Ok(None)
127 }
128}
129
130#[derive(Debug, Default)]
133pub struct MemorySource {
134 manifests: Vec<Vec<u8>>,
135 blobs: std::collections::BTreeMap<String, Vec<u8>>,
136 line_status: Option<Vec<u8>>,
137 line_index: Option<Vec<u8>>,
138 served: Option<Vec<String>>,
139 attestations: Vec<crate::attestcarry::CarriedAttestation>,
140}
141
142impl MemorySource {
143 pub fn new() -> Self {
144 Self::default()
145 }
146
147 pub fn with_manifest(mut self, bytes: &[u8]) -> Self {
148 self.manifests.push(bytes.to_vec());
149 self
150 }
151
152 pub fn with_blob(mut self, digest: &str, bytes: &[u8]) -> Self {
153 self.blobs.insert(digest.to_string(), bytes.to_vec());
154 self
155 }
156
157 pub fn with_line_index(mut self, envelope: &[u8]) -> Self {
161 self.line_index = Some(envelope.to_vec());
162 self
163 }
164
165 pub fn serving(mut self, layers: &[&str]) -> Self {
169 self.served = Some(layers.iter().map(|s| s.to_string()).collect());
170 self
171 }
172
173 pub fn with_line_status(mut self, envelope: &[u8]) -> Self {
174 self.line_status = Some(envelope.to_vec());
175 self
176 }
177
178 pub fn with_attestation(mut self, statement: &[u8], attested_bytes: &[u8]) -> Self {
183 self.attestations
184 .push(crate::attestcarry::CarriedAttestation {
185 statement_digest: crate::store::manifest_digest(statement),
186 statement: statement.to_vec(),
187 bytes: attested_bytes.to_vec(),
188 });
189 self
190 }
191}
192
193#[derive(Debug)]
198pub struct DirSource {
199 root: std::path::PathBuf,
200}
201
202impl DirSource {
203 pub fn at(root: impl Into<std::path::PathBuf>) -> Self {
204 DirSource { root: root.into() }
205 }
206
207 pub fn put(&self, manifest_bytes: &[u8], blobs: &[(&str, &[u8])]) -> std::io::Result<()> {
210 let manifests = self.root.join("manifests");
211 let blob_dir = self.root.join("blobs");
212 std::fs::create_dir_all(&manifests)?;
213 std::fs::create_dir_all(&blob_dir)?;
214 let digest = crate::store::manifest_digest(manifest_bytes);
215 std::fs::write(manifests.join(digest.replace(':', "-")), manifest_bytes)?;
216 for (digest, bytes) in blobs {
217 std::fs::write(blob_dir.join(digest.replace(':', "-")), bytes)?;
218 }
219 Ok(())
220 }
221}
222
223impl LayerSource for DirSource {
224 fn fetch_manifest(&self, layer: &LayerRef) -> Result<Vec<u8>, SourceError> {
225 let dir = self.root.join("manifests");
226 let entries = std::fs::read_dir(&dir)
227 .map_err(|e| SourceError::Transport(format!("{}: {e}", dir.display())))?;
228 for entry in entries.filter_map(|e| e.ok()) {
229 let bytes =
230 std::fs::read(entry.path()).map_err(|e| SourceError::Transport(e.to_string()))?;
231 if discovery_matches(&bytes, layer) {
232 return Ok(bytes);
233 }
234 }
235 Err(SourceError::NotFound(format!("{layer:?}")))
236 }
237
238 fn fetch_blob(&self, digest: &str) -> Result<Vec<u8>, SourceError> {
239 let path = self.root.join("blobs").join(digest.replace(':', "-"));
240 match std::fs::read(&path) {
241 Ok(bytes) => Ok(bytes),
242 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
243 Err(SourceError::NotFound(digest.to_string()))
244 }
245 Err(e) => Err(SourceError::Transport(e.to_string())),
246 }
247 }
248
249 }
262
263impl LayerSource for MemorySource {
264 fn fetch_manifest(&self, layer: &LayerRef) -> Result<Vec<u8>, SourceError> {
265 self.manifests
266 .iter()
267 .find(|bytes| discovery_matches(bytes, layer))
268 .cloned()
269 .ok_or_else(|| SourceError::NotFound(format!("{layer:?}")))
270 }
271
272 fn fetch_blob(&self, digest: &str) -> Result<Vec<u8>, SourceError> {
273 self.blobs
274 .get(digest)
275 .cloned()
276 .ok_or_else(|| SourceError::NotFound(digest.to_string()))
277 }
278
279 fn fetch_line_index(&self, _line: &str) -> Result<Option<Vec<u8>>, SourceError> {
280 Ok(self.line_index.clone())
281 }
282
283 fn served_layers(&self, _line: &str) -> Result<Option<Vec<String>>, SourceError> {
284 Ok(self.served.clone())
285 }
286
287 fn fetch_line_status(&self, _layer: &LayerRef) -> Result<Option<Vec<u8>>, SourceError> {
288 Ok(self.line_status.clone())
289 }
290
291 fn fetch_attestations(
292 &self,
293 _layer: &LayerRef,
294 ) -> Result<Vec<crate::attestcarry::CarriedAttestation>, SourceError> {
295 Ok(self.attestations.clone())
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 #[test]
305 fn a_source_carrying_a_baseline_line_status_yields_it() {
306 let envelope = b"an-opaque-dsse-envelope";
307 let source = MemorySource::new().with_line_status(envelope);
308 let got = source
309 .fetch_line_status(&LayerRef::Name("2026.07.0".parse().unwrap()))
310 .unwrap();
311 assert_eq!(
312 got.as_deref(),
313 Some(envelope.as_slice()),
314 "a source that carries a baseline line-status must hand it back for caching"
315 );
316 }
317
318 #[test]
320 fn a_source_carrying_attestations_hands_over_both_blobs_and_one_without_is_not_an_error() {
321 let source = MemorySource::new().with_attestation(b"a-statement-envelope", b"the-evidence");
322 let got = source
323 .fetch_attestations(&LayerRef::Name("2026.07.0".parse().unwrap()))
324 .unwrap();
325 assert_eq!(got.len(), 1);
326 assert_eq!(
327 got[0].bytes, b"the-evidence",
328 "the attested bytes must travel beside the statement — a claim with nothing to \
329 check it against is what crossing the air gap must never produce"
330 );
331 assert_eq!(
332 got[0].statement_digest,
333 crate::store::manifest_digest(b"a-statement-envelope"),
334 "the digest is derived from the bytes; a source never declares its own address"
335 );
336
337 assert!(
341 MemorySource::new()
342 .fetch_attestations(&LayerRef::Name("2026.07.0".parse().unwrap()))
343 .unwrap()
344 .is_empty()
345 );
346 }
347
348 #[test]
350 fn a_source_without_a_line_status_is_not_an_error() {
351 let source = MemorySource::new();
352 let got = source
353 .fetch_line_status(&LayerRef::Name("2026.07.0".parse().unwrap()))
354 .unwrap();
355 assert_eq!(
356 got, None,
357 "an absent line-status is Ok(None), never an error"
358 );
359 }
360}