1use std::io::{self, Read, Write};
27
28const MAGIC: &[u8; 8] = b"CXSTATE\0";
29const VERSION: u16 = 1;
30const FLAG_FOUR_STATE: u16 = 1 << 0;
31const FLAG_SCHEDULE: u16 = 1 << 1;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum StateRole {
36 State,
38 Comb,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct StateObject {
45 pub path: String,
46 pub width: usize,
47 pub role: StateRole,
48 pub is_4state: bool,
49 pub value: Vec<u8>,
51 pub mask: Option<Vec<u8>>,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct ScheduledEvent {
59 pub time: u64,
60 pub event: String,
62 pub signal: String,
64 pub value: u8,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Default)]
69pub struct ScheduleRecord {
70 pub time: u64,
71 pub clocks: Vec<(String, u64)>,
73 pub events: Vec<ScheduledEvent>,
75 pub periodic: Vec<(ScheduledEvent, u64)>,
78 pub high_events: Vec<String>,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Default)]
84pub struct StateFile {
85 pub four_state: bool,
86 pub objects: Vec<StateObject>,
87 pub schedule: Option<ScheduleRecord>,
88}
89
90#[derive(Debug, thiserror::Error)]
92pub enum StateFileError {
93 #[error(transparent)]
94 Io(#[from] io::Error),
95 #[error("not a Celox state file")]
96 BadMagic,
97 #[error("unsupported state file version {0} (this build reads version {VERSION})")]
98 UnsupportedVersion(u16),
99 #[error("malformed state file: {0}")]
100 Malformed(String),
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
105pub enum StateDifference {
106 Changed {
108 path: String,
109 left: StateObject,
110 right: StateObject,
111 },
112 OnlyLeft(StateObject),
114 OnlyRight(StateObject),
116}
117
118impl StateFile {
119 pub fn write_to(&self, mut writer: impl Write) -> io::Result<()> {
120 let w = &mut writer;
121 w.write_all(MAGIC)?;
122 put_u16(w, VERSION)?;
123 let mut flags = 0;
124 if self.four_state {
125 flags |= FLAG_FOUR_STATE;
126 }
127 if self.schedule.is_some() {
128 flags |= FLAG_SCHEDULE;
129 }
130 put_u16(w, flags)?;
131 put_len(w, self.objects.len())?;
132 for object in &self.objects {
133 put_str(w, &object.path)?;
134 put_u64(w, object.width as u64)?;
135 w.write_all(&[
136 match object.role {
137 StateRole::State => 0,
138 StateRole::Comb => 1,
139 },
140 object.is_4state as u8,
141 ])?;
142 let size = object.width.div_ceil(8);
143 assert_eq!(object.value.len(), size, "value size of `{}`", object.path);
144 w.write_all(&object.value)?;
145 if self.four_state && object.is_4state {
146 let mask = object.mask.as_deref().unwrap_or_default();
147 assert_eq!(mask.len(), size, "mask size of `{}`", object.path);
148 w.write_all(mask)?;
149 }
150 }
151 if let Some(schedule) = &self.schedule {
152 put_u64(w, schedule.time)?;
153 put_len(w, schedule.clocks.len())?;
154 for (event, period) in &schedule.clocks {
155 put_str(w, event)?;
156 put_u64(w, *period)?;
157 }
158 put_len(w, schedule.events.len())?;
159 for event in &schedule.events {
160 put_event(w, event)?;
161 }
162 put_len(w, schedule.periodic.len())?;
163 for (event, count) in &schedule.periodic {
164 put_event(w, event)?;
165 put_u64(w, *count)?;
166 }
167 put_len(w, schedule.high_events.len())?;
168 for event in &schedule.high_events {
169 put_str(w, event)?;
170 }
171 }
172 Ok(())
173 }
174
175 pub fn read_from(mut reader: impl Read) -> Result<Self, StateFileError> {
176 let r = &mut reader;
177 let mut magic = [0; 8];
178 r.read_exact(&mut magic)?;
179 if &magic != MAGIC {
180 return Err(StateFileError::BadMagic);
181 }
182 let version = get_u16(r)?;
183 if version != VERSION {
184 return Err(StateFileError::UnsupportedVersion(version));
185 }
186 let flags = get_u16(r)?;
187 let four_state = flags & FLAG_FOUR_STATE != 0;
188 let count = get_u32(r)?;
189 let mut objects = Vec::new();
190 for _ in 0..count {
191 let path = get_str(r)?;
192 let width = usize::try_from(get_u64(r)?)
193 .map_err(|_| StateFileError::Malformed(format!("width of `{path}`")))?;
194 let mut tags = [0; 2];
195 r.read_exact(&mut tags)?;
196 let role = match tags[0] {
197 0 => StateRole::State,
198 1 => StateRole::Comb,
199 other => {
200 return Err(StateFileError::Malformed(format!(
201 "role {other} of `{path}`"
202 )));
203 }
204 };
205 let is_4state = tags[1] != 0;
206 let size = width.div_ceil(8);
207 let value = get_bytes(r, size)?;
208 let mask = if four_state && is_4state {
209 Some(get_bytes(r, size)?)
210 } else {
211 None
212 };
213 objects.push(StateObject {
214 path,
215 width,
216 role,
217 is_4state,
218 value,
219 mask,
220 });
221 }
222 let schedule = if flags & FLAG_SCHEDULE != 0 {
223 let time = get_u64(r)?;
224 let clocks = (0..get_u32(r)?)
225 .map(|_| Ok((get_str(r)?, get_u64(r)?)))
226 .collect::<Result<_, StateFileError>>()?;
227 let events = (0..get_u32(r)?)
228 .map(|_| get_event(r))
229 .collect::<Result<_, _>>()?;
230 let periodic = (0..get_u32(r)?)
231 .map(|_| Ok((get_event(r)?, get_u64(r)?)))
232 .collect::<Result<_, StateFileError>>()?;
233 let high_events = (0..get_u32(r)?)
234 .map(|_| get_str(r))
235 .collect::<Result<_, _>>()?;
236 Some(ScheduleRecord {
237 time,
238 clocks,
239 events,
240 periodic,
241 high_events,
242 })
243 } else {
244 None
245 };
246 let mut rest = [0; 1];
247 if r.read(&mut rest)? != 0 {
248 return Err(StateFileError::Malformed(
249 "trailing bytes after the last section".into(),
250 ));
251 }
252 Ok(Self {
253 four_state,
254 objects,
255 schedule,
256 })
257 }
258
259 pub fn diff(&self, other: &Self, include_comb: bool) -> Vec<StateDifference> {
265 use std::collections::BTreeMap;
266
267 let left: BTreeMap<_, _> = self.objects.iter().map(|o| (&o.path, o)).collect();
268 let right: BTreeMap<_, _> = other.objects.iter().map(|o| (&o.path, o)).collect();
269 let compared = |object: &StateObject| include_comb || object.role == StateRole::State;
270 let mut differences = Vec::new();
271 for (path, l) in &left {
272 match right.get(path) {
273 Some(r) => {
274 if !(compared(l) && compared(r)) {
275 continue;
276 }
277 let lmask = l.mask.as_deref().unwrap_or_default();
278 let rmask = r.mask.as_deref().unwrap_or_default();
279 if l.width != r.width || l.value != r.value || !masks_equal(lmask, rmask) {
280 differences.push(StateDifference::Changed {
281 path: (*path).clone(),
282 left: (*l).clone(),
283 right: (*r).clone(),
284 });
285 }
286 }
287 None if compared(l) => differences.push(StateDifference::OnlyLeft((*l).clone())),
288 None => {}
289 }
290 }
291 for (path, r) in &right {
292 if !left.contains_key(path) && compared(r) {
293 differences.push(StateDifference::OnlyRight((*r).clone()));
294 }
295 }
296 differences
297 }
298}
299
300fn masks_equal(left: &[u8], right: &[u8]) -> bool {
302 let len = left.len().max(right.len());
303 (0..len).all(|i| left.get(i).copied().unwrap_or(0) == right.get(i).copied().unwrap_or(0))
304}
305
306pub fn format_state_value(value: &[u8], mask: Option<&[u8]>, width: usize) -> String {
309 let digits = width.div_ceil(4).max(1);
310 (0..digits)
311 .rev()
312 .map(|digit| {
313 let nibble = |bytes: &[u8]| {
314 let byte = bytes.get(digit / 2).copied().unwrap_or(0);
315 (byte >> ((digit % 2) * 4)) & 0xf
316 };
317 if mask.is_some_and(|mask| nibble(mask) != 0) {
318 'x'
319 } else {
320 char::from_digit(u32::from(nibble(value)), 16).unwrap()
321 }
322 })
323 .collect()
324}
325
326fn put_u16(w: &mut impl Write, value: u16) -> io::Result<()> {
327 w.write_all(&value.to_le_bytes())
328}
329
330fn put_u64(w: &mut impl Write, value: u64) -> io::Result<()> {
331 w.write_all(&value.to_le_bytes())
332}
333
334fn put_len(w: &mut impl Write, len: usize) -> io::Result<()> {
335 let len = u32::try_from(len)
336 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "too many entries"))?;
337 w.write_all(&len.to_le_bytes())
338}
339
340fn put_str(w: &mut impl Write, value: &str) -> io::Result<()> {
341 put_len(w, value.len())?;
342 w.write_all(value.as_bytes())
343}
344
345fn put_event(w: &mut impl Write, event: &ScheduledEvent) -> io::Result<()> {
346 put_u64(w, event.time)?;
347 put_str(w, &event.event)?;
348 put_str(w, &event.signal)?;
349 w.write_all(&[event.value])
350}
351
352fn get_u16(r: &mut impl Read) -> io::Result<u16> {
353 let mut bytes = [0; 2];
354 r.read_exact(&mut bytes)?;
355 Ok(u16::from_le_bytes(bytes))
356}
357
358fn get_u32(r: &mut impl Read) -> io::Result<u32> {
359 let mut bytes = [0; 4];
360 r.read_exact(&mut bytes)?;
361 Ok(u32::from_le_bytes(bytes))
362}
363
364fn get_u64(r: &mut impl Read) -> io::Result<u64> {
365 let mut bytes = [0; 8];
366 r.read_exact(&mut bytes)?;
367 Ok(u64::from_le_bytes(bytes))
368}
369
370fn get_bytes(r: &mut impl Read, len: usize) -> io::Result<Vec<u8>> {
371 let mut bytes = Vec::new();
374 r.take(len as u64).read_to_end(&mut bytes)?;
375 if bytes.len() != len {
376 return Err(io::ErrorKind::UnexpectedEof.into());
377 }
378 Ok(bytes)
379}
380
381fn get_str(r: &mut impl Read) -> Result<String, StateFileError> {
382 let len = get_u32(r)? as usize;
383 String::from_utf8(get_bytes(r, len)?)
384 .map_err(|_| StateFileError::Malformed("path is not UTF-8".into()))
385}
386
387fn get_event(r: &mut impl Read) -> Result<ScheduledEvent, StateFileError> {
388 let time = get_u64(r)?;
389 let event = get_str(r)?;
390 let signal = get_str(r)?;
391 let mut value = [0; 1];
392 r.read_exact(&mut value)?;
393 Ok(ScheduledEvent {
394 time,
395 event,
396 signal,
397 value: value[0],
398 })
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404
405 fn object(path: &str, role: StateRole, value: u8) -> StateObject {
406 StateObject {
407 path: path.into(),
408 width: 8,
409 role,
410 is_4state: true,
411 value: vec![value],
412 mask: Some(vec![0]),
413 }
414 }
415
416 fn sample() -> StateFile {
417 let event = ScheduledEvent {
418 time: 15,
419 event: "clk".into(),
420 signal: "clk".into(),
421 value: 1,
422 };
423 StateFile {
424 four_state: true,
425 objects: vec![
426 object("q", StateRole::State, 3),
427 StateObject {
428 path: "wide".into(),
429 width: 70,
430 role: StateRole::State,
431 is_4state: false,
432 value: (0..9).collect(),
433 mask: None,
434 },
435 object("y", StateRole::Comb, 4),
436 ],
437 schedule: Some(ScheduleRecord {
438 time: 10,
439 clocks: vec![("clk".into(), 10)],
440 events: vec![event.clone()],
441 periodic: vec![(event, 1)],
442 high_events: vec!["clk".into()],
443 }),
444 }
445 }
446
447 #[test]
448 fn round_trips() {
449 let file = sample();
450 let mut bytes = Vec::new();
451 file.write_to(&mut bytes).unwrap();
452 assert_eq!(StateFile::read_from(bytes.as_slice()).unwrap(), file);
453 }
454
455 #[test]
456 fn rejects_foreign_and_truncated_input() {
457 assert!(matches!(
458 StateFile::read_from(&b"not a state file"[..]),
459 Err(StateFileError::BadMagic)
460 ));
461 let mut bytes = Vec::new();
462 sample().write_to(&mut bytes).unwrap();
463 for len in [10, bytes.len() - 1] {
464 assert!(StateFile::read_from(&bytes[..len]).is_err());
465 }
466 bytes.push(0);
467 assert!(matches!(
468 StateFile::read_from(bytes.as_slice()),
469 Err(StateFileError::Malformed(_))
470 ));
471 }
472
473 #[test]
474 fn diff_ignores_combinational_objects_unless_asked() {
475 let left = sample();
476 let mut right = sample();
477 right.objects[0].value = vec![5];
478 right.objects[2].value = vec![9];
479 let paths = |differences: Vec<StateDifference>| -> Vec<String> {
480 differences
481 .into_iter()
482 .map(|difference| match difference {
483 StateDifference::Changed { path, .. } => path,
484 StateDifference::OnlyLeft(o) | StateDifference::OnlyRight(o) => o.path,
485 })
486 .collect()
487 };
488 assert_eq!(paths(left.diff(&right, false)), ["q"]);
489 assert_eq!(paths(left.diff(&right, true)), ["q", "y"]);
490
491 right.objects[2].role = StateRole::State;
494 assert_eq!(paths(left.diff(&right, false)), ["q"]);
495 }
496
497 #[test]
498 fn formats_values_with_unknown_bits() {
499 assert_eq!(format_state_value(&[0x3c, 0x01], None, 12), "13c");
500 assert_eq!(format_state_value(&[0x3c], Some(&[0xf0]), 8), "xc");
501 }
502}