1use 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#[derive(Serialize, Deserialize, Debug, Clone)]
17pub struct HashedMir {
18 spec: RtLolaMir,
20 hash: [u8; 32],
22}
23
24#[derive(Debug, Clone, Copy)]
26pub enum HashError {
27 HashMismatch {
29 imported_hash: [u8; 32],
31 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 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 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}