Skip to main content

cherenkov/model/index/
catalog.rs

1use 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}