walker_common/
store.rs

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    /// The data to store
25    pub data: &'a [u8],
26    /// An optional SHA256 digest
27    pub sha256: &'a Option<RetrievedDigest<Sha256>>,
28    /// An optional SHA512 digest
29    pub sha512: &'a Option<RetrievedDigest<Sha512>>,
30    /// An optional signature
31    pub signature: &'a Option<String>,
32
33    /// Last change date
34    pub changed: SystemTime,
35
36    /// Metadata from the retrieval process
37    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        // We use the retrieval metadata timestamp as file timestamp. If that's not available, then
83        // we use the change entry timestamp.
84        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}