1use std::io::{Read, Write};
4use std::path::PathBuf;
5
6use chrono::Utc;
7use serde::{Deserialize, Serialize};
8
9use crate::approval::{ApprovalDecision, ApprovalRequest, ApprovalRule};
10use crate::condition::Condition;
11use crate::error::{ContractError, ContractResult};
12use crate::obligation::Obligation;
13use crate::policy::Policy;
14use crate::risk_limit::RiskLimit;
15use crate::violation::Violation;
16use crate::ContractId;
17
18pub const MAGIC: [u8; 4] = *b"ACON";
20
21pub const VERSION: u32 = 1;
23
24#[derive(Debug, Clone)]
26#[repr(C)]
27pub struct FileHeader {
28 pub magic: [u8; 4],
30 pub version: u32,
32 pub flags: u32,
34 pub policy_count: u64,
36 pub risk_limit_count: u64,
38 pub approval_rule_count: u64,
40 pub approval_request_count: u64,
42 pub condition_count: u64,
44 pub obligation_count: u64,
46 pub violation_count: u64,
48 pub created_at: u64,
50 pub modified_at: u64,
52 pub checksum: [u8; 32],
54}
55
56impl Default for FileHeader {
57 fn default() -> Self {
58 Self::new()
59 }
60}
61
62impl FileHeader {
63 pub fn new() -> Self {
65 let now = Utc::now().timestamp_micros() as u64;
66 Self {
67 magic: MAGIC,
68 version: VERSION,
69 flags: 0,
70 policy_count: 0,
71 risk_limit_count: 0,
72 approval_rule_count: 0,
73 approval_request_count: 0,
74 condition_count: 0,
75 obligation_count: 0,
76 violation_count: 0,
77 created_at: now,
78 modified_at: now,
79 checksum: [0; 32],
80 }
81 }
82
83 pub fn write_to<W: Write>(&self, writer: &mut W) -> ContractResult<()> {
85 writer.write_all(&self.magic)?;
86 writer.write_all(&self.version.to_le_bytes())?;
87 writer.write_all(&self.flags.to_le_bytes())?;
88 writer.write_all(&self.policy_count.to_le_bytes())?;
89 writer.write_all(&self.risk_limit_count.to_le_bytes())?;
90 writer.write_all(&self.approval_rule_count.to_le_bytes())?;
91 writer.write_all(&self.approval_request_count.to_le_bytes())?;
92 writer.write_all(&self.condition_count.to_le_bytes())?;
93 writer.write_all(&self.obligation_count.to_le_bytes())?;
94 writer.write_all(&self.violation_count.to_le_bytes())?;
95 writer.write_all(&self.created_at.to_le_bytes())?;
96 writer.write_all(&self.modified_at.to_le_bytes())?;
97 writer.write_all(&self.checksum)?;
98 Ok(())
99 }
100
101 pub fn read_from<R: Read>(reader: &mut R) -> ContractResult<Self> {
103 let mut magic = [0u8; 4];
104 reader.read_exact(&mut magic)?;
105
106 if magic != MAGIC {
107 return Err(ContractError::FileFormat(format!(
108 "Invalid magic bytes: {:?} (expected {:?}). File may be corrupted.",
109 magic, MAGIC
110 )));
111 }
112
113 let mut buf4 = [0u8; 4];
114 let mut buf8 = [0u8; 8];
115 let mut checksum = [0u8; 32];
116
117 reader.read_exact(&mut buf4)?;
118 let version = u32::from_le_bytes(buf4);
119
120 reader.read_exact(&mut buf4)?;
121 let flags = u32::from_le_bytes(buf4);
122
123 reader.read_exact(&mut buf8)?;
124 let policy_count = u64::from_le_bytes(buf8);
125
126 reader.read_exact(&mut buf8)?;
127 let risk_limit_count = u64::from_le_bytes(buf8);
128
129 reader.read_exact(&mut buf8)?;
130 let approval_rule_count = u64::from_le_bytes(buf8);
131
132 reader.read_exact(&mut buf8)?;
133 let approval_request_count = u64::from_le_bytes(buf8);
134
135 reader.read_exact(&mut buf8)?;
136 let condition_count = u64::from_le_bytes(buf8);
137
138 reader.read_exact(&mut buf8)?;
139 let obligation_count = u64::from_le_bytes(buf8);
140
141 reader.read_exact(&mut buf8)?;
142 let violation_count = u64::from_le_bytes(buf8);
143
144 reader.read_exact(&mut buf8)?;
145 let created_at = u64::from_le_bytes(buf8);
146
147 reader.read_exact(&mut buf8)?;
148 let modified_at = u64::from_le_bytes(buf8);
149
150 reader.read_exact(&mut checksum)?;
151
152 Ok(Self {
153 magic,
154 version,
155 flags,
156 policy_count,
157 risk_limit_count,
158 approval_rule_count,
159 approval_request_count,
160 condition_count,
161 obligation_count,
162 violation_count,
163 created_at,
164 modified_at,
165 checksum,
166 })
167 }
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172#[repr(u8)]
173pub enum EntityType {
174 Policy = 1,
176 RiskLimit = 2,
178 ApprovalRule = 3,
180 ApprovalRequest = 4,
182 ApprovalDecision = 5,
184 Condition = 6,
186 Obligation = 7,
188 Violation = 8,
190}
191
192impl TryFrom<u8> for EntityType {
193 type Error = ContractError;
194
195 fn try_from(value: u8) -> Result<Self, Self::Error> {
196 match value {
197 1 => Ok(EntityType::Policy),
198 2 => Ok(EntityType::RiskLimit),
199 3 => Ok(EntityType::ApprovalRule),
200 4 => Ok(EntityType::ApprovalRequest),
201 5 => Ok(EntityType::ApprovalDecision),
202 6 => Ok(EntityType::Condition),
203 7 => Ok(EntityType::Obligation),
204 8 => Ok(EntityType::Violation),
205 _ => Err(ContractError::FileFormat(format!(
206 "Unknown entity type: {}",
207 value
208 ))),
209 }
210 }
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct ContractFile {
216 #[serde(default)]
218 pub policies: Vec<Policy>,
219 #[serde(default)]
221 pub risk_limits: Vec<RiskLimit>,
222 #[serde(default)]
224 pub approval_rules: Vec<ApprovalRule>,
225 #[serde(default)]
227 pub approval_requests: Vec<ApprovalRequest>,
228 #[serde(default)]
230 pub approval_decisions: Vec<ApprovalDecision>,
231 #[serde(default)]
233 pub conditions: Vec<Condition>,
234 #[serde(default)]
236 pub obligations: Vec<Obligation>,
237 #[serde(default)]
239 pub violations: Vec<Violation>,
240 #[serde(skip)]
242 pub path: Option<PathBuf>,
243}
244
245impl Default for ContractFile {
246 fn default() -> Self {
247 Self::new()
248 }
249}
250
251impl ContractFile {
252 pub fn new() -> Self {
254 Self {
255 policies: vec![],
256 risk_limits: vec![],
257 approval_rules: vec![],
258 approval_requests: vec![],
259 approval_decisions: vec![],
260 conditions: vec![],
261 obligations: vec![],
262 violations: vec![],
263 path: None,
264 }
265 }
266
267 pub fn open(path: impl Into<PathBuf>) -> ContractResult<Self> {
269 let path = path.into();
270 if path.exists() {
271 Self::load(&path)
272 } else {
273 let mut file = Self::new();
274 file.path = Some(path);
275 Ok(file)
276 }
277 }
278
279 pub fn load(path: &std::path::Path) -> ContractResult<Self> {
281 let mut reader = std::io::BufReader::new(std::fs::File::open(path)?);
282 let _header = FileHeader::read_from(&mut reader)?;
283
284 let mut body = String::new();
286 reader.read_to_string(&mut body)?;
287
288 let mut file: ContractFile = serde_json::from_str(&body)?;
289 file.path = Some(path.to_path_buf());
290 Ok(file)
291 }
292
293 pub fn save(&self) -> ContractResult<()> {
295 let path = self
296 .path
297 .as_ref()
298 .ok_or_else(|| ContractError::FileFormat("No file path set".to_string()))?;
299
300 if let Some(parent) = path.parent() {
302 std::fs::create_dir_all(parent)?;
303 }
304
305 let mut writer = std::io::BufWriter::new(std::fs::File::create(path)?);
306
307 let mut header = FileHeader::new();
308 header.policy_count = self.policies.len() as u64;
309 header.risk_limit_count = self.risk_limits.len() as u64;
310 header.approval_rule_count = self.approval_rules.len() as u64;
311 header.approval_request_count = self.approval_requests.len() as u64;
312 header.condition_count = self.conditions.len() as u64;
313 header.obligation_count = self.obligations.len() as u64;
314 header.violation_count = self.violations.len() as u64;
315
316 let body = serde_json::to_string(self)?;
318 header.checksum = *blake3::hash(body.as_bytes()).as_bytes();
319
320 header.write_to(&mut writer)?;
321 writer.write_all(body.as_bytes())?;
322
323 Ok(())
324 }
325
326 pub fn total_entities(&self) -> usize {
328 self.policies.len()
329 + self.risk_limits.len()
330 + self.approval_rules.len()
331 + self.approval_requests.len()
332 + self.approval_decisions.len()
333 + self.conditions.len()
334 + self.obligations.len()
335 + self.violations.len()
336 }
337
338 pub fn find_policy(&self, id: ContractId) -> Option<&Policy> {
340 self.policies.iter().find(|p| p.id == id)
341 }
342
343 pub fn find_risk_limit(&self, id: ContractId) -> Option<&RiskLimit> {
345 self.risk_limits.iter().find(|r| r.id == id)
346 }
347
348 pub fn find_risk_limit_mut(&mut self, id: ContractId) -> Option<&mut RiskLimit> {
350 self.risk_limits.iter_mut().find(|r| r.id == id)
351 }
352
353 pub fn find_obligation(&self, id: ContractId) -> Option<&Obligation> {
355 self.obligations.iter().find(|o| o.id == id)
356 }
357
358 pub fn find_obligation_mut(&mut self, id: ContractId) -> Option<&mut Obligation> {
360 self.obligations.iter_mut().find(|o| o.id == id)
361 }
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367
368 #[test]
369 fn test_header_roundtrip() {
370 let header = FileHeader::new();
371 let mut buf = Vec::new();
372 header.write_to(&mut buf).unwrap();
373
374 let parsed = FileHeader::read_from(&mut buf.as_slice()).unwrap();
375 assert_eq!(parsed.magic, MAGIC);
376 assert_eq!(parsed.version, VERSION);
377 }
378
379 #[test]
380 fn test_contract_file_new() {
381 let file = ContractFile::new();
382 assert_eq!(file.total_entities(), 0);
383 assert!(file.policies.is_empty());
384 }
385
386 #[test]
387 fn test_bad_magic() {
388 let bad = b"BADMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
389 let result = FileHeader::read_from(&mut bad.as_slice());
390 assert!(result.is_err());
391 }
392}