entrouter_universal/
lib.rs1use base64::{engine::general_purpose::STANDARD, Engine};
25use sha2::{Digest, Sha256};
26use thiserror::Error;
27
28pub mod chain;
29pub mod envelope;
30pub mod guardian;
31pub mod signed_envelope;
32pub mod universal_struct;
33pub mod verify;
34
35#[cfg(feature = "compression")]
36pub mod compress;
37
38pub use chain::Chain;
39pub use chain::ChainDiff;
40pub use envelope::Envelope;
41pub use guardian::Guardian;
42pub use signed_envelope::SignedEnvelope;
43pub use universal_struct::UniversalStruct;
44pub use verify::VerifyResult;
45
46#[derive(Debug, Clone, PartialEq, Error)]
50#[non_exhaustive]
51pub enum UniversalError {
52 #[error("Integrity violation: data was mutated in transit. Expected {expected}, got {actual}")]
53 IntegrityViolation { expected: String, actual: String },
54
55 #[error("Decode error: {0}")]
56 DecodeError(String),
57
58 #[error("Envelope malformed: {0}")]
59 MalformedEnvelope(String),
60
61 #[error("Expired: envelope expired at {expired_at}, current time {now}")]
62 Expired { expired_at: u64, now: u64 },
63
64 #[error("Compress error: {0}")]
65 CompressError(String),
66
67 #[error("Serialization error: {0}")]
68 SerializationError(String),
69
70 #[error("Chain merge conflict: chains diverge at link {diverges_at}")]
71 ChainMergeConflict { diverges_at: usize },
72}
73
74#[must_use]
83pub fn encode(input: &[u8]) -> String {
84 STANDARD.encode(input)
85}
86
87pub fn decode(input: &str) -> Result<Vec<u8>, UniversalError> {
89 STANDARD
90 .decode(input)
91 .map_err(|e| UniversalError::DecodeError(e.to_string()))
92}
93
94#[must_use]
101pub fn encode_str(input: &str) -> String {
102 encode(input.as_bytes())
103}
104
105pub fn decode_str(input: &str) -> Result<String, UniversalError> {
107 let bytes = decode(input)?;
108 String::from_utf8(bytes).map_err(|e| UniversalError::DecodeError(e.to_string()))
109}
110
111#[must_use]
118pub fn fingerprint(input: &[u8]) -> String {
119 let mut hasher = Sha256::new();
120 hasher.update(input);
121 hex::encode(hasher.finalize())
122}
123
124#[must_use]
126pub fn fingerprint_str(input: &str) -> String {
127 fingerprint(input.as_bytes())
128}
129
130pub fn verify(encoded: &str, original_fingerprint: &str) -> Result<VerifyResult, UniversalError> {
135 let decoded = decode(encoded)?;
136 let actual_fingerprint = fingerprint(&decoded);
137 if actual_fingerprint == original_fingerprint {
138 Ok(VerifyResult {
139 intact: true,
140 decoded,
141 fingerprint: actual_fingerprint,
142 })
143 } else {
144 Err(UniversalError::IntegrityViolation {
145 expected: original_fingerprint.to_string(),
146 actual: actual_fingerprint,
147 })
148 }
149}
150
151#[cfg(test)]
156mod tests {
157 use super::*;
158 use std::thread::sleep;
159 use std::time::Duration;
160
161 #[test]
164 fn round_trip_special_chars() {
165 let original = r#"hello "world" it's \fine\ with 日本語 and 🔥"#;
166 assert_eq!(original, decode_str(&encode_str(original)).unwrap());
167 }
168
169 #[test]
172 fn envelope_standard() {
173 let data = r#"{"token":"abc\"def","user":"john's"}"#;
174 let env = Envelope::wrap(data);
175 assert_eq!(data, env.unwrap_verified().unwrap());
176 }
177
178 #[test]
179 fn envelope_url_safe() {
180 let data = "race_token: abc\"123\"\nspecial chars & stuff";
181 let env = Envelope::wrap_url_safe(data);
182 assert!(env
184 .d
185 .chars()
186 .all(|c| c.is_alphanumeric() || c == '-' || c == '_'));
187 assert_eq!(data, env.unwrap_verified().unwrap());
188 }
189
190 #[cfg(feature = "compression")]
191 #[test]
192 fn envelope_compressed() {
193 let data = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".repeat(100);
195 let env = Envelope::wrap_compressed(&data).unwrap();
196 assert!(env.d.len() < data.len());
198 assert_eq!(data, env.unwrap_verified().unwrap());
199 }
200
201 #[test]
202 fn envelope_ttl_valid() {
203 let env = Envelope::wrap_with_ttl("fresh data", 60);
204 assert!(!env.is_expired());
205 assert_eq!("fresh data", env.unwrap_verified().unwrap());
206 }
207
208 #[test]
209 fn envelope_ttl_expired() {
210 let env = Envelope::wrap_with_ttl("stale data", 0);
211 sleep(Duration::from_millis(10));
212 assert!(env.is_expired());
213 assert!(env.unwrap_verified().is_err());
214 }
215
216 #[test]
217 fn envelope_detects_mutation() {
218 let env = Envelope::wrap("original");
219 let mut json = env.to_json().unwrap();
220 let idx = json.find('"').unwrap() + 5;
222 json.replace_range(idx..idx + 1, "X");
223 let tampered = Envelope::from_json(&json);
224 let result = tampered.and_then(|e| e.unwrap_verified());
225 assert!(result.is_err());
226 }
227
228 #[test]
231 fn chain_builds_and_verifies() {
232 let mut chain = Chain::new("genesis: race started");
233 chain.append("link 2: user_a joined");
234 chain.append("link 3: user_b joined");
235 chain.append("link 4: winner = user_a");
236
237 let result = chain.verify();
238 assert!(result.valid);
239 assert_eq!(result.total_links, 4);
240 }
241
242 #[test]
243 fn chain_detects_tampering() {
244 let mut chain = Chain::new("genesis");
245 chain.append("link 2");
246 chain.append("link 3");
247
248 let mut tampered = chain.clone();
250 tampered.links[1].d = encode_str("TAMPERED");
251
252 let result = tampered.verify();
253 assert!(!result.valid);
254 assert_eq!(result.broken_at, Some(2));
255 }
256
257 #[test]
258 fn chain_serialises_round_trip() {
259 let mut chain = Chain::new("start");
260 chain.append("middle");
261 chain.append("end");
262
263 let json = chain.to_json().unwrap();
264 let restored = Chain::from_json(&json).unwrap();
265 assert!(restored.verify().valid);
266 }
267
268 #[test]
271 fn struct_wraps_all_fields() {
272 let wrapped = UniversalStruct::wrap_fields(&[
273 ("token", "000001739850123456-abc\"def"),
274 ("user_id", "john's account"),
275 ("amount", "99.99"),
276 ]);
277
278 let result = wrapped.verify_all();
279 assert!(result.all_intact);
280 assert_eq!(wrapped.get("token").unwrap(), "000001739850123456-abc\"def");
281 assert_eq!(wrapped.get("user_id").unwrap(), "john's account");
282 assert_eq!(wrapped.get("amount").unwrap(), "99.99");
283 }
284
285 #[test]
286 fn struct_detects_field_mutation() {
287 let mut wrapped = UniversalStruct::wrap_fields(&[
288 ("token", "abc123"),
289 ("user_id", "john"),
290 ("amount", "99.99"),
291 ]);
292
293 wrapped.fields[2].d = encode_str("999999.99");
295
296 let result = wrapped.verify_all();
297 assert!(!result.all_intact);
298 assert!(result.violations.contains(&"amount".to_string()));
299 assert!(result.fields[0].intact);
301 assert!(result.fields[1].intact);
302 assert!(!result.fields[2].intact);
303 }
304
305 #[test]
306 fn struct_to_map() {
307 let wrapped = UniversalStruct::wrap_fields(&[("a", "hello"), ("b", "world")]);
308 let map = wrapped.to_map().unwrap();
309 assert_eq!(map["a"], "hello");
310 assert_eq!(map["b"], "world");
311 }
312
313 #[test]
314 fn struct_serialises_round_trip() {
315 let wrapped =
316 UniversalStruct::wrap_fields(&[("token", r#"abc"def\ghi"#), ("user", "john")]);
317 let json = wrapped.to_json().unwrap();
318 let restored = UniversalStruct::from_json(&json).unwrap();
319 restored.assert_intact();
320 assert_eq!(restored.get("token").unwrap(), r#"abc"def\ghi"#);
321 }
322
323 #[test]
326 fn guardian_clean_pipeline() {
327 let mut g = Guardian::new("clean data 🔥");
328 let encoded = g.encoded().to_string();
329 g.checkpoint("http", &encoded);
330 g.checkpoint("redis", &encoded);
331 g.checkpoint("postgres", &encoded);
332 g.assert_intact();
333 }
334
335 #[test]
336 fn guardian_finds_violation() {
337 let mut g = Guardian::new("original");
338 let clean = g.encoded().to_string();
339 g.checkpoint("http", &clean);
340 g.checkpoint("redis", &encode_str("mangled"));
341 assert_eq!(g.first_violation().unwrap().layer, "redis");
342 }
343}