Skip to main content

isothrum_runner/
manifest.rs

1//! Reading a run manifest from its file: the contract's own parser (`isoloom-manifest`, which
2//! checks the version, the types and what spans fields), plus where it lives and its hash, which
3//! a run's record keeps.
4
5use std::path::{Path, PathBuf};
6
7use isoloom_manifest::Manifest;
8use sha2::{Digest, Sha256};
9
10use crate::Result;
11use crate::error::Error;
12
13/// A manifest read from its file.
14#[derive(Debug, Clone)]
15#[non_exhaustive]
16pub struct LoadedManifest {
17    /// The manifest.
18    pub manifest: Manifest,
19    /// Its file, absolute.
20    pub path: PathBuf,
21    /// Its folder, absolute: `${run.dir}`, and the folder its relative paths start from.
22    pub dir: PathBuf,
23    /// The SHA-256 of its bytes, in lowercase hex.
24    pub sha256: String,
25}
26
27/// Reads and checks the manifest at `path`.
28///
29/// # Errors
30///
31/// Fails with `io` when the file can't be read, and with `manifest_invalid` or `manifest_version`
32/// when the contract refuses it (the error names the field and the fix).
33pub fn load_manifest(path: &Path) -> Result<LoadedManifest> {
34    let path = std::path::absolute(path).map_err(Error::io(path, "read"))?;
35    let bytes = std::fs::read(&path).map_err(Error::io(&path, "read"))?;
36    let text = String::from_utf8_lossy(&bytes);
37    let manifest = isoloom_manifest::parse(&text).map_err(|source| Error::Manifest { path: path.clone(), source })?;
38    let dir = path.parent().map_or_else(|| path.clone(), Path::to_path_buf);
39    Ok(LoadedManifest {
40        manifest,
41        sha256: sha256(&bytes),
42        path,
43        dir,
44    })
45}
46
47/// `bytes`' SHA-256, in lowercase hex.
48fn sha256(bytes: &[u8]) -> String {
49    Sha256::digest(bytes)
50        .iter()
51        .flat_map(|b| [b >> 4, b & 0xF])
52        .filter_map(|nibble| char::from_digit(u32::from(nibble), 16))
53        .collect()
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn the_hash_is_the_files_sha256() {
62        assert_eq!(sha256(b"abc"), "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
63    }
64
65    #[test]
66    fn a_missing_file_is_an_io_error_naming_it() {
67        let err = load_manifest(Path::new("no/such/run.json")).unwrap_err();
68        assert_eq!(err.code(), crate::ErrorCode::Io);
69        assert!(err.to_string().contains("run.json couldn't be read"), "{err}");
70    }
71}