1use std::path::{Path, PathBuf};
19
20use anyhow::{bail, Context, Result};
21use chrono::{DateTime, Utc};
22use holger_update_bundle::atomic_write;
23use holger_update_bundle::digest::sha256_hex;
24use serde::{Deserialize, Serialize};
25
26use crate::set::AdvisorySet;
27
28pub const INSTALLED_SCHEMA: &str = "holger.intel-installed/v1";
30
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
33pub struct InstalledIntel {
34 pub schema: String,
35 pub bundle_id: String,
36 pub sequence: u64,
37 pub supplier: String,
38 pub snapshot_at: DateTime<Utc>,
41 pub imported_at: DateTime<Utc>,
42 pub candidate_digest: String,
43 pub release_signer_fingerprint: String,
44 pub advisories_file: String,
46 pub advisories_sha256: String,
47 pub advisories: u64,
48}
49
50impl InstalledIntel {
51 pub fn describe(&self) -> String {
53 format!("seq {} dated {}", self.sequence, self.snapshot_at.format("%Y-%m-%d"))
54 }
55}
56
57#[derive(Debug, Clone)]
59pub struct LocalIntelDb {
60 root: PathBuf,
61}
62
63impl LocalIntelDb {
64 pub fn open(root: impl AsRef<Path>) -> Self {
65 Self { root: root.as_ref().to_path_buf() }
66 }
67
68 pub fn root(&self) -> &Path {
69 &self.root
70 }
71 pub fn state_dir(&self) -> PathBuf {
72 self.root.join("state")
73 }
74 pub fn bundles_dir(&self) -> PathBuf {
75 self.root.join("bundles")
76 }
77 pub fn receipts_dir(&self) -> PathBuf {
78 self.root.join("receipts")
79 }
80 pub fn lock_path(&self) -> PathBuf {
81 self.root.join(".intel-import.lock")
82 }
83 fn installed_path(&self) -> PathBuf {
84 self.root.join("installed.json")
85 }
86
87 pub fn generation_file(sequence: u64) -> String {
89 format!("advisories-{sequence}.json")
90 }
91
92 pub fn installed(&self) -> Result<Option<InstalledIntel>> {
94 let p = self.installed_path();
95 let bytes = match std::fs::read(&p) {
96 Ok(b) => b,
97 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
98 Err(e) => return Err(e).with_context(|| format!("read {}", p.display())),
99 };
100 let meta: InstalledIntel = serde_json::from_slice(&bytes)
101 .with_context(|| format!("{} is not an intel install record", p.display()))?;
102 if meta.schema != INSTALLED_SCHEMA {
103 bail!("{} has schema {:?}, expected {INSTALLED_SCHEMA:?}", p.display(), meta.schema);
104 }
105 Ok(Some(meta))
106 }
107
108 pub fn load(&self) -> Result<Option<(InstalledIntel, AdvisorySet)>> {
110 let Some(meta) = self.installed()? else {
111 return Ok(None);
112 };
113 if meta.advisories_file.contains('/') || meta.advisories_file.contains("..") {
114 bail!("intel install record names an unsafe file {:?}", meta.advisories_file);
115 }
116 let p = self.root.join(&meta.advisories_file);
117 let bytes = std::fs::read(&p).with_context(|| format!("read {}", p.display()))?;
118 let got = sha256_hex(&bytes);
119 if got != meta.advisories_sha256 {
120 bail!(
121 "the local advisory database at {} does not match its install record \
122 (sha256 {got}, recorded {}): it was changed after import. Re-import a \
123 signed intel bundle.",
124 p.display(),
125 meta.advisories_sha256
126 );
127 }
128 let set = AdvisorySet::from_json_bytes(&bytes)?;
129 Ok(Some((meta, set)))
130 }
131
132 pub fn stage_generation(&self, sequence: u64, bytes: &[u8]) -> Result<PathBuf> {
134 std::fs::create_dir_all(&self.root).with_context(|| format!("mkdir {}", self.root.display()))?;
135 let p = self.root.join(Self::generation_file(sequence));
136 atomic_write(&p, bytes).map_err(|e| anyhow::anyhow!("write {}: {e}", p.display()))?;
137 Ok(p)
138 }
139
140 pub fn activate(&self, meta: &InstalledIntel) -> Result<()> {
143 let bytes = serde_json::to_vec_pretty(meta)?;
144 let previous = self.installed().ok().flatten().map(|m| m.advisories_file);
145 atomic_write(&self.installed_path(), &bytes)
146 .map_err(|e| anyhow::anyhow!("activate intel generation: {e}"))?;
147 if let Ok(rd) = std::fs::read_dir(&self.root) {
148 for e in rd.flatten() {
149 let n = e.file_name().to_string_lossy().into_owned();
150 let is_gen = n.starts_with("advisories-") && n.ends_with(".json");
151 if is_gen && n != meta.advisories_file && Some(&n) != previous.as_ref() {
152 let _ = std::fs::remove_file(e.path());
153 }
154 }
155 }
156 Ok(())
157 }
158}