rtlola_frontend/
hash.rs

1//! Allows attaching a hash to an [RtLolaMir]. Used when exporting/importing
2//! an [RtLolaMir] with serde::Serialize/Deserialize and check, whether the
3//! frontend version or specification did not change or whether the specification
4//! has to be parsed again.
5
6use std::fmt::Write;
7
8use rtlola_parser::ParserConfig;
9use rtlola_reporting::Diagnostic;
10use serde::{Deserialize, Serialize};
11use sha2::{Digest, Sha256};
12
13use crate::RtLolaMir;
14
15/// Represents an [RtLolaMir] with a hash of the specification and frontend version attached to it.
16#[derive(Serialize, Deserialize, Debug, Clone)]
17pub struct HashedMir {
18    // the intermediate representation of the specification
19    spec: RtLolaMir,
20    /// the hash of the specification and frontend version
21    hash: [u8; 32],
22}
23
24/// Describes an error that can happen when checking [HashedMir]'s.
25#[derive(Debug, Clone, Copy)]
26pub enum HashError {
27    /// The file that was exported does not have the same hash as the imported one
28    HashMismatch {
29        /// the hash of the specification that was imported
30        imported_hash: [u8; 32],
31        /// the hash of the current specification
32        current_hash: [u8; 32],
33    },
34}
35
36impl From<HashError> for Diagnostic {
37    fn from(error: HashError) -> Self {
38        match error {
39            HashError::HashMismatch {
40                imported_hash,
41                current_hash,
42            } => {
43                let imported_hash = imported_hash.iter().fold(String::new(), |mut s, byte| {
44                    write!(&mut s, "{:02X}", byte).expect("write to string can never fail");
45                    s
46                });
47                let current_hash = current_hash.iter().fold(String::new(), |mut s, byte| {
48                    write!(&mut s, "{:02X}", byte).expect("write to string can never fail");
49                    s
50                });
51                Diagnostic::error(&format!("The imported file was exported from a specification with hash {imported_hash}, but the current specification has hash {current_hash}."))
52            }
53        }
54    }
55}
56
57fn hash_spec(config: &ParserConfig) -> [u8; 32] {
58    Sha256::new()
59        .chain_update(config.spec())
60        .chain_update(env!("CARGO_PKG_VERSION_MAJOR"))
61        .chain_update(".")
62        .chain_update(env!("CARGO_PKG_VERSION_MINOR"))
63        .finalize()
64        .into()
65}
66
67impl HashedMir {
68    /// Checks the hash of the [HashedMir].
69    pub fn check(self, config: &ParserConfig) -> Result<RtLolaMir, HashError> {
70        let Self { spec, hash } = self;
71        let current_hash = hash_spec(config);
72        if hash != current_hash {
73            return Err(HashError::HashMismatch {
74                imported_hash: hash,
75                current_hash,
76            });
77        }
78        Ok(spec)
79    }
80}
81
82impl RtLolaMir {
83    /// Adds a hash of the specification and frontend version to the mir.
84    pub fn hash(self, config: &ParserConfig) -> HashedMir {
85        let hash = hash_spec(config);
86        HashedMir { spec: self, hash }
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use rtlola_parser::ParserConfig;
93
94    use super::HashedMir;
95    use crate::{parse, RtLolaMir};
96
97    fn to_json(mir: HashedMir) -> Result<Vec<u8>, serde_json::Error> {
98        let mut v = Vec::new();
99        serde_json::to_writer(&mut v, &mir)?;
100        Ok(v)
101    }
102
103    fn from_json(v: Vec<u8>) -> Result<HashedMir, serde_json::Error> {
104        serde_json::from_reader(&v[..])
105    }
106
107    #[test]
108    fn export_and_import() {
109        let spec = "input a : UInt64\ninput b : UInt64\noutput c := a + b\ntrigger c > 5 \"test\"";
110        let config = ParserConfig::for_string(spec.into());
111        let mir = parse(&config).expect("should parse");
112
113        let exported_mir = mir.hash(&config);
114
115        let json = to_json(exported_mir).expect("should be able to export");
116
117        let imported: RtLolaMir = from_json(json)
118            .expect("should parse")
119            .check(&config)
120            .expect("should pass");
121
122        let mir = parse(&config).expect("should parse");
123
124        assert_eq!(mir, imported);
125    }
126
127    #[test]
128    fn wrong_checksum() {
129        let spec = "input a : UInt64\ninput b : UInt64\noutput c := a + b\ntrigger c > 5 \"test\"";
130        let config = ParserConfig::for_string(spec.into());
131        let mir = parse(&config).expect("should parse");
132
133        let exported_mir = mir.hash(&config);
134        let exported = to_json(exported_mir).unwrap();
135
136        let new_spec =
137            "input a : UInt64\ninput b : UInt64\noutput c := a + b\ntrigger c > 10 \"test\"";
138        let new_config = ParserConfig::for_string(new_spec.into());
139
140        let imported = from_json(exported).expect("should parse");
141
142        imported
143            .clone()
144            .check(&new_config)
145            .expect_err("should fail because checksum");
146    }
147}