Skip to main content

pacenotes_server/store/
manifest_file.rs

1/// Reads and validates `manifest.json`; a missing file is an empty manifest.
2///
3/// Rejects the whole document when any game, directory or name is not a valid
4/// lookup key, so a hand-edited manifest can never yield a path outside the data root.
5pub fn read_manifest(path: &std::path::Path) -> anyhow::Result<pacenotes_shared::Manifest> {
6    let bytes = match std::fs::read(path) {
7        Ok(bytes) => bytes,
8        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
9            return Ok(pacenotes_shared::Manifest::default())
10        }
11        Err(e) => anyhow::bail!("reading {}: {e}", path.display()),
12    };
13    let manifest: pacenotes_shared::Manifest = serde_json::from_slice(&bytes)
14        .map_err(|e| anyhow::anyhow!("parsing {}: {e}", path.display()))?;
15
16    for package in &manifest.packages {
17        if !pacenotes_shared::valid_id(&package.game) {
18            anyhow::bail!("invalid game id {:?} in manifest", package.game);
19        }
20        if !pacenotes_shared::valid_id(&package.directory) {
21            anyhow::bail!("invalid directory name {:?} in manifest", package.directory);
22        }
23        if !pacenotes_shared::valid_name(&package.name) {
24            anyhow::bail!("invalid package name {:?} in manifest", package.name);
25        }
26    }
27    Ok(manifest)
28}
29
30/// Writes `<data>/manifest.json` through a temp file renamed over it in the same
31/// directory, so a crash or a full disk leaves the previous manifest whole and
32/// a reloading server never reads half a file.
33pub fn write_manifest(
34    data_dir: &std::path::Path,
35    manifest: &pacenotes_shared::Manifest,
36) -> anyhow::Result<std::path::PathBuf> {
37    let path = data_dir.join(crate::store::MANIFEST_FILE);
38    let temp = data_dir.join(format!(".{}.tmp", crate::store::MANIFEST_FILE));
39    let json = serde_json::to_string_pretty(manifest)?;
40    std::fs::write(&temp, format!("{json}\n"))
41        .map_err(|e| anyhow::anyhow!("writing {}: {e}", temp.display()))?;
42    std::fs::rename(&temp, &path).map_err(|e| {
43        let _ = std::fs::remove_file(&temp);
44        anyhow::anyhow!("replacing {}: {e}", path.display())
45    })?;
46    Ok(path)
47}
48
49#[cfg(test)]
50mod tests {
51    #[test]
52    fn the_manifest_is_replaced_without_leaving_a_temp_file() {
53        let tmp = tempfile::tempdir().unwrap();
54        let dir = tmp.path();
55        std::fs::write(dir.join(crate::store::MANIFEST_FILE), b"old").unwrap();
56
57        let path = super::write_manifest(dir, &pacenotes_shared::Manifest::default()).unwrap();
58        assert_eq!(path, dir.join(crate::store::MANIFEST_FILE));
59        assert!(super::read_manifest(&path).unwrap().packages.is_empty());
60        let leftovers: Vec<_> = std::fs::read_dir(dir).unwrap().collect();
61        assert_eq!(leftovers.len(), 1, "{leftovers:?}");
62    }
63}