Skip to main content

hayson_engine/catalog/
write.rs

1use std::path::Path;
2
3use crate::atomic::write_atomic;
4use crate::errors::HaysonSchemaError;
5
6use super::parse::parse_catalog;
7use super::serialize::{catalog_document, serialize_catalog};
8use super::types::Catalog;
9
10#[cfg(unix)]
11const PERMISSION_BITS: u32 = 0o777;
12
13/// Parses the catalog's own serialized form back before it is allowed near the disk — a catalog
14/// this build could write but not read back would leave the next open() unable to see its tables.
15pub fn assert_catalog_readable(path: &Path, catalog: &Catalog) -> Result<(), HaysonSchemaError> {
16    parse_catalog(&catalog_document(catalog)).map(|_| ()).map_err(|error| {
17        HaysonSchemaError(format!(
18            "refusing to write \"{}\": the catalog it would hold is one this build of hayson cannot read back — {}; nothing on disk was changed, and the declaration in the way has to go before the change can",
19            path.display(),
20            error.0,
21        ))
22    })
23}
24
25#[cfg(unix)]
26fn permissions_of(path: &Path) -> std::io::Result<Option<u32>> {
27    use std::os::unix::fs::PermissionsExt;
28    match std::fs::metadata(path) {
29        Ok(metadata) => Ok(Some(metadata.permissions().mode() & PERMISSION_BITS)),
30        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
31        Err(error) => Err(error),
32    }
33}
34
35// NOTE: wasi has no permission bits at all; the host sandbox bounds fs access instead.
36#[cfg(not(unix))]
37fn permissions_of(_path: &Path) -> std::io::Result<Option<u32>> {
38    Ok(None)
39}
40
41/// Writes the catalog atomically (`.tmp` → `fsync` → `rename`, via [`write_atomic`]), inheriting
42/// the permissions of the file it replaces so `.tmp` is never briefly world-readable
43/// (see `docs/format.md`, "Zapis, kolejność kluczy, uprawnienia"). Refuses first, via
44/// [`assert_catalog_readable`], leaving the file on disk untouched.
45pub fn write_catalog(path: &Path, catalog: &Catalog) -> Result<(), Box<dyn std::error::Error>> {
46    assert_catalog_readable(path, catalog)?;
47    let mode = permissions_of(path)?;
48    let contents = serialize_catalog(catalog);
49    write_atomic(path, contents.as_bytes(), mode)?;
50    Ok(())
51}
52
53#[cfg(test)]
54mod tests {
55    use std::collections::BTreeMap;
56    use std::fs;
57
58    use super::*;
59    use crate::catalog::build::empty_catalog;
60    use crate::catalog::types::CatalogLayout;
61
62    #[test]
63    fn writes_the_catalog_atomically() {
64        let dir = tempfile::tempdir().unwrap();
65        let path = dir.path().join("hayson.json");
66        let catalog = empty_catalog(CatalogLayout::Flat, 1_000);
67
68        write_catalog(&path, &catalog).unwrap();
69
70        let on_disk = fs::read_to_string(&path).unwrap();
71        assert!(on_disk.contains("\"catalogVersion\": 2"));
72        assert!(!path.with_extension("json.tmp").exists());
73
74        let reparsed = parse_catalog(&serde_json::from_str(&on_disk).unwrap()).unwrap();
75        assert_eq!(reparsed.tables, BTreeMap::new());
76    }
77
78    #[test]
79    fn refuses_before_touching_disk_when_the_catalog_cannot_be_read_back() {
80        let dir = tempfile::tempdir().unwrap();
81        let path = dir.path().join("hayson.json");
82        fs::write(&path, "original").unwrap();
83
84        let mut catalog = empty_catalog(CatalogLayout::Flat, 1_000);
85        catalog.catalog_version = 3;
86
87        assert!(write_catalog(&path, &catalog).is_err());
88        assert_eq!(fs::read_to_string(&path).unwrap(), "original");
89    }
90}