cherenkov/model/index/
catalog.rs1use super::{Catalog, ModelIndex};
2use anyhow::{Context, Result, ensure};
3use std::{fs::File, io::Write, path::Component};
4
5pub(super) struct LockedCatalog {
6 pub value: Catalog,
7 index: ModelIndex,
8 _lock: File,
9}
10
11impl ModelIndex {
12 pub(super) fn lock(&self) -> Result<LockedCatalog> {
13 crate::storage::create_private_dir(&self.paths.data)?;
14
15 let lock = File::options()
16 .create(true)
17 .truncate(false)
18 .write(true)
19 .open(self.paths.data.join("index.lock"))?;
20
21 lock.lock()?;
22
23 let path = self.paths.data.join("index.json");
24 let value: Catalog = match std::fs::read(&path) {
25 Ok(bytes) => serde_json::from_slice(&bytes).context("reading model index")?,
26 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Catalog::default(),
27 Err(e) => return Err(e.into()),
28 };
29
30 validate(&value)?;
31
32 Ok(LockedCatalog {
33 value,
34 index: self.clone(),
35 _lock: lock,
36 })
37 }
38}
39
40impl LockedCatalog {
41 pub fn save(&self) -> Result<()> {
42 let temporary = self.index.paths.data.join("index.json.tmp");
43 let mut file = File::create(&temporary)?;
44
45 serde_json::to_writer(&mut file, &self.value)?;
46 file.write_all(b"\n")?;
47 file.sync_all()?;
48 std::fs::rename(temporary, self.index.paths.data.join("index.json"))?;
49 File::open(&self.index.paths.data)?.sync_all()?;
50
51 Ok(())
52 }
53}
54
55fn validate(catalog: &Catalog) -> Result<()> {
56 ensure!(
57 catalog.version == 1,
58 "unsupported model index version {}",
59 catalog.version
60 );
61
62 for (id, entry) in &catalog.models {
63 ensure!(valid_id(id) && id == &entry.id, "invalid model ID");
64
65 for artifact in entry.prepared.iter().chain(&entry.retained_source) {
66 ensure!(
67 catalog.artifacts.contains_key(artifact),
68 "missing indexed artifact {artifact}"
69 );
70 }
71 }
72
73 for (name, store) in &catalog.stores {
74 ensure!(
75 name == &store.name && crate::storage::safe_component(name),
76 "invalid store name"
77 );
78 ensure!(
79 valid_id(&store.id) && store.path.is_absolute(),
80 "invalid store registration"
81 );
82 }
83
84 for (id, artifact) in &catalog.artifacts {
85 ensure!(valid_id(id) && id == &artifact.id, "invalid artifact ID");
86 ensure!(
87 artifact
88 .entry
89 .components()
90 .all(|c| matches!(c, Component::Normal(_))),
91 "artifact entry must stay within its store"
92 );
93
94 if let Some(path) = &artifact.external {
95 ensure!(
96 path.is_absolute(),
97 "external artifact path must be absolute"
98 );
99 }
100 }
101
102 Ok(())
103}
104
105fn valid_id(id: &str) -> bool {
106 id.len() == 32 && id.bytes().all(|b| b.is_ascii_hexdigit())
107}