1use crate::retrieve::{RetrievalMetadata, RetrievedDigest};
2use anyhow::Context;
3use sha2::{Sha256, Sha512};
4use std::path::Path;
5use std::time::SystemTime;
6use tokio::fs;
7
8#[cfg(target_os = "macos")]
9pub const ATTR_ETAG: &str = "etag";
10#[cfg(target_os = "linux")]
11pub const ATTR_ETAG: &str = "user.etag";
12
13#[derive(Debug, thiserror::Error)]
14pub enum StoreError {
15 #[error("{0:#}")]
16 Io(anyhow::Error),
17 #[error("Failed to construct filename from URL: {0}")]
18 Filename(String),
19 #[error("Serialize key error: {0:#}")]
20 SerializeKey(anyhow::Error),
21}
22
23pub struct Document<'a> {
24 pub data: &'a [u8],
26 pub sha256: &'a Option<RetrievedDigest<Sha256>>,
28 pub sha512: &'a Option<RetrievedDigest<Sha512>>,
30 pub signature: &'a Option<String>,
32
33 pub changed: SystemTime,
35
36 pub metadata: &'a RetrievalMetadata,
38
39 pub no_timestamps: bool,
40 #[cfg(any(target_os = "linux", target_os = "macos"))]
41 pub no_xattrs: bool,
42}
43
44pub async fn store_document(file: &Path, document: Document<'_>) -> Result<(), StoreError> {
45 log::debug!("Writing {}", file.display());
46
47 if let Some(parent) = file.parent() {
48 fs::create_dir_all(parent)
49 .await
50 .with_context(|| format!("Failed to create parent directory: {}", parent.display()))
51 .map_err(StoreError::Io)?;
52 }
53
54 fs::write(&file, document.data)
55 .await
56 .with_context(|| format!("Failed to write advisory: {}", file.display()))
57 .map_err(StoreError::Io)?;
58
59 if let Some(sha256) = &document.sha256 {
60 let file = format!("{}.sha256", file.display());
61 fs::write(&file, &sha256.expected)
62 .await
63 .with_context(|| format!("Failed to write checksum: {file}"))
64 .map_err(StoreError::Io)?;
65 }
66 if let Some(sha512) = &document.sha512 {
67 let file = format!("{}.sha512", file.display());
68 fs::write(&file, &sha512.expected)
69 .await
70 .with_context(|| format!("Failed to write checksum: {file}"))
71 .map_err(StoreError::Io)?;
72 }
73 if let Some(sig) = &document.signature {
74 let file = format!("{}.asc", file.display());
75 fs::write(&file, &sig)
76 .await
77 .with_context(|| format!("Failed to write signature: {file}"))
78 .map_err(StoreError::Io)?;
79 }
80
81 if !document.no_timestamps {
82 let mtime = document
85 .metadata
86 .last_modification
87 .map(SystemTime::from)
88 .unwrap_or_else(|| document.changed)
89 .into();
90 filetime::set_file_mtime(file, mtime)
91 .with_context(|| {
92 format!(
93 "Failed to set last modification timestamp: {}",
94 file.display()
95 )
96 })
97 .map_err(StoreError::Io)?;
98 }
99
100 #[cfg(any(target_os = "linux", target_os = "macos"))]
101 if !document.no_xattrs {
102 if let Some(etag) = &document.metadata.etag {
103 xattr::set(file, ATTR_ETAG, etag.as_bytes())
104 .with_context(|| format!("Failed to store {}: {}", ATTR_ETAG, file.display()))
105 .map_err(StoreError::Io)?;
106 }
107 }
108
109 Ok(())
110}