use serde::{Deserialize, Serialize};
use std::io::{Read, Write};
pub const SCHEMA_VERSION: u8 = 1;
pub const LOG_MAGIC: &[u8; 4] = b"RUQU";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SyndromeRound {
pub round_id: u64,
pub timestamp_ns: u64,
pub code_distance: u8,
pub events: Vec<DetectorEvent>,
#[serde(default)]
pub metadata: RoundMetadata,
}
impl SyndromeRound {
pub fn new(round_id: u64, code_distance: u8) -> Self {
Self {
round_id,
timestamp_ns: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0),
code_distance,
events: Vec::new(),
metadata: RoundMetadata::default(),
}
}
pub fn add_event(&mut self, event: DetectorEvent) {
self.events.push(event);
}
pub fn fired_count(&self) -> usize {
self.events.iter().filter(|e| e.fired).count()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DetectorEvent {
pub detector_id: u32,
pub fired: bool,
#[serde(default = "default_confidence")]
pub confidence: f32,
#[serde(default)]
pub coords: Option<DetectorCoords>,
}
fn default_confidence() -> f32 {
1.0
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
pub struct DetectorCoords {
pub x: i16,
pub y: i16,
pub t: i16,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct RoundMetadata {
#[serde(default)]
pub source: String,
#[serde(default)]
pub error_rate: Option<f64>,
#[serde(default)]
pub is_hardware: bool,
#[serde(default)]
pub injected_fault: Option<String>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum BoundaryId {
Left,
Right,
Top,
Bottom,
Virtual,
Custom(u32),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PermitToken {
pub token_id: u64,
pub issued_at_round: u64,
pub issued_at_ns: u64,
pub ttl_ns: u64,
pub region_mask: u64,
pub confidence: f32,
pub min_cut_value: f32,
}
impl PermitToken {
pub fn is_valid(&self, current_time_ns: u64) -> bool {
current_time_ns < self.issued_at_ns.saturating_add(self.ttl_ns)
}
pub fn remaining_ttl_ns(&self, current_time_ns: u64) -> u64 {
self.issued_at_ns
.saturating_add(self.ttl_ns)
.saturating_sub(current_time_ns)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct GateDecision {
pub round_id: u64,
pub timestamp_ns: u64,
pub decision: DecisionType,
pub latency_ns: u64,
pub metrics: GateMetrics,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum DecisionType {
Permit(PermitToken),
Defer {
wait_ns: u64,
uncertainty: f32,
},
Deny {
risk_level: f32,
affected_regions: u64,
},
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct GateMetrics {
pub min_cut: f32,
pub cut_std: f32,
pub shift: f32,
pub evidence: f32,
pub fired_count: u32,
pub clustering: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MitigationAction {
pub action_id: u64,
pub timestamp_ns: u64,
pub action_type: ActionTypeSchema,
pub target_regions: Vec<u32>,
pub duration_ns: u64,
pub result: ActionResult,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum ActionTypeSchema {
QuarantineRegion,
IncreaseSyndromeRounds,
SwitchDecodeMode,
TriggerReweight,
PauseLearningWrites,
LogEvent,
AlertOperator,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ActionResult {
Success,
Partial {
completed: f32,
},
Failed {
reason: String,
},
Pending,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum RecordType {
SyndromeRound = 1,
GateDecision = 2,
MitigationAction = 3,
Checkpoint = 4,
Config = 5,
Metrics = 6,
}
impl TryFrom<u8> for RecordType {
type Error = ();
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
1 => Ok(RecordType::SyndromeRound),
2 => Ok(RecordType::GateDecision),
3 => Ok(RecordType::MitigationAction),
4 => Ok(RecordType::Checkpoint),
5 => Ok(RecordType::Config),
6 => Ok(RecordType::Metrics),
_ => Err(()),
}
}
}
pub struct LogWriter<W: Write> {
writer: W,
record_count: u64,
}
impl<W: Write> LogWriter<W> {
pub fn new(mut writer: W) -> std::io::Result<Self> {
writer.write_all(LOG_MAGIC)?;
writer.write_all(&[SCHEMA_VERSION])?;
Ok(Self {
writer,
record_count: 0,
})
}
pub fn write_syndrome(&mut self, round: &SyndromeRound) -> std::io::Result<()> {
let payload = serde_json::to_vec(round).map_err(|e| {
std::io::Error::new(std::io::ErrorKind::InvalidData, e)
})?;
self.write_record(RecordType::SyndromeRound, &payload)
}
pub fn write_decision(&mut self, decision: &GateDecision) -> std::io::Result<()> {
let payload = serde_json::to_vec(decision).map_err(|e| {
std::io::Error::new(std::io::ErrorKind::InvalidData, e)
})?;
self.write_record(RecordType::GateDecision, &payload)
}
pub fn write_action(&mut self, action: &MitigationAction) -> std::io::Result<()> {
let payload = serde_json::to_vec(action).map_err(|e| {
std::io::Error::new(std::io::ErrorKind::InvalidData, e)
})?;
self.write_record(RecordType::MitigationAction, &payload)
}
fn write_record(&mut self, record_type: RecordType, payload: &[u8]) -> std::io::Result<()> {
self.writer.write_all(&[record_type as u8])?;
let len = payload.len() as u32;
self.writer.write_all(&len.to_le_bytes())?;
self.writer.write_all(payload)?;
let crc = crc32fast::hash(payload);
self.writer.write_all(&crc.to_le_bytes())?;
self.record_count += 1;
Ok(())
}
pub fn finish(mut self) -> std::io::Result<u64> {
self.writer.flush()?;
Ok(self.record_count)
}
}
pub struct LogReader<R: Read> {
reader: R,
version: u8,
}
impl<R: Read> LogReader<R> {
pub fn new(mut reader: R) -> std::io::Result<Self> {
let mut magic = [0u8; 4];
reader.read_exact(&mut magic)?;
if &magic != LOG_MAGIC {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Invalid magic header",
));
}
let mut version = [0u8; 1];
reader.read_exact(&mut version)?;
Ok(Self {
reader,
version: version[0],
})
}
pub fn version(&self) -> u8 {
self.version
}
pub fn read_record(&mut self) -> std::io::Result<Option<LogRecord>> {
let mut type_byte = [0u8; 1];
match self.reader.read_exact(&mut type_byte) {
Ok(()) => (),
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
Err(e) => return Err(e),
}
let record_type = RecordType::try_from(type_byte[0]).map_err(|_| {
std::io::Error::new(std::io::ErrorKind::InvalidData, "Unknown record type")
})?;
let mut len_bytes = [0u8; 4];
self.reader.read_exact(&mut len_bytes)?;
let len = u32::from_le_bytes(len_bytes) as usize;
let mut payload = vec![0u8; len];
self.reader.read_exact(&mut payload)?;
let mut crc_bytes = [0u8; 4];
self.reader.read_exact(&mut crc_bytes)?;
let stored_crc = u32::from_le_bytes(crc_bytes);
let computed_crc = crc32fast::hash(&payload);
if stored_crc != computed_crc {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"CRC mismatch",
));
}
let record = match record_type {
RecordType::SyndromeRound => {
let round: SyndromeRound = serde_json::from_slice(&payload).map_err(|e| {
std::io::Error::new(std::io::ErrorKind::InvalidData, e)
})?;
LogRecord::Syndrome(round)
}
RecordType::GateDecision => {
let decision: GateDecision = serde_json::from_slice(&payload).map_err(|e| {
std::io::Error::new(std::io::ErrorKind::InvalidData, e)
})?;
LogRecord::Decision(decision)
}
RecordType::MitigationAction => {
let action: MitigationAction = serde_json::from_slice(&payload).map_err(|e| {
std::io::Error::new(std::io::ErrorKind::InvalidData, e)
})?;
LogRecord::Action(action)
}
_ => LogRecord::Unknown(payload),
};
Ok(Some(record))
}
}
#[derive(Debug, Clone)]
pub enum LogRecord {
Syndrome(SyndromeRound),
Decision(GateDecision),
Action(MitigationAction),
Unknown(Vec<u8>),
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn test_syndrome_round() {
let mut round = SyndromeRound::new(1, 5);
round.add_event(DetectorEvent {
detector_id: 0,
fired: true,
confidence: 0.99,
coords: Some(DetectorCoords { x: 0, y: 0, t: 0 }),
});
round.add_event(DetectorEvent {
detector_id: 1,
fired: false,
confidence: 1.0,
coords: None,
});
assert_eq!(round.fired_count(), 1);
}
#[test]
fn test_permit_token_validity() {
let token = PermitToken {
token_id: 1,
issued_at_round: 100,
issued_at_ns: 1000000,
ttl_ns: 100000,
region_mask: 0xFF,
confidence: 0.95,
min_cut_value: 5.5,
};
assert!(token.is_valid(1050000));
assert!(!token.is_valid(1200000));
assert_eq!(token.remaining_ttl_ns(1050000), 50000);
}
#[test]
fn test_log_roundtrip() {
let mut buffer = Vec::new();
{
let mut writer = LogWriter::new(&mut buffer).unwrap();
let round = SyndromeRound::new(1, 5);
writer.write_syndrome(&round).unwrap();
writer.finish().unwrap();
}
{
let mut reader = LogReader::new(Cursor::new(&buffer)).unwrap();
assert_eq!(reader.version(), SCHEMA_VERSION);
let record = reader.read_record().unwrap().unwrap();
match record {
LogRecord::Syndrome(round) => {
assert_eq!(round.round_id, 1);
assert_eq!(round.code_distance, 5);
}
_ => panic!("Expected syndrome record"),
}
}
}
}