use byteorder::{ByteOrder, LittleEndian};
use std::path::Path;
const HEADER_SIZE: usize = 68;
const RECORD_STRIDE: usize = 20;
#[derive(Debug, Clone, PartialEq)]
pub struct ActualsRecord {
pub actual_id: u32,
pub retention_time_min: f64,
}
pub fn parse(path: &Path) -> crate::Result<Vec<ActualsRecord>> {
let bytes = std::fs::read(path)?;
if bytes.len() < HEADER_SIZE {
return Ok(vec![]);
}
let payload = &bytes[HEADER_SIZE..];
let count = payload.len() / RECORD_STRIDE;
let mut records = Vec::with_capacity(count);
for i in 0..count {
let offset = i * RECORD_STRIDE;
let record_bytes = &payload[offset..offset + RECORD_STRIDE];
let actual_id = LittleEndian::read_u32(&record_bytes[4..8]);
let retention_time_min = LittleEndian::read_f64(&record_bytes[8..16]);
records.push(ActualsRecord {
actual_id,
retention_time_min,
});
}
Ok(records)
}
#[cfg(test)]
mod tests {
use super::*;
fn record_bytes(leading: u32, actual_id: u32, time_min: f64, trailing: u32) -> Vec<u8> {
let mut buf = Vec::with_capacity(RECORD_STRIDE);
buf.extend_from_slice(&leading.to_le_bytes());
buf.extend_from_slice(&actual_id.to_le_bytes());
buf.extend_from_slice(&time_min.to_le_bytes());
buf.extend_from_slice(&trailing.to_le_bytes());
buf
}
fn write_temp(name: &str, contents: &[u8]) -> std::path::PathBuf {
let mut path = std::env::temp_dir();
path.push(format!(
"openaraw-msperiodicactuals-test-{}-{}",
std::process::id(),
name
));
std::fs::write(&path, contents).expect("write temp file");
path
}
#[test]
fn parses_actual_id_and_time_from_records() {
let mut bytes = vec![0u8; HEADER_SIZE];
bytes.extend(record_bytes(1, 27, 0.0334, 0));
bytes.extend(record_bytes(0, 27, 5.512, 0));
bytes.extend(record_bytes(0, 29, 110.0038, 0));
bytes.extend_from_slice(&[0u8; 4]);
let path = write_temp("basic.bin", &bytes);
let records = parse(&path).expect("parse should succeed");
let _ = std::fs::remove_file(&path);
assert_eq!(records.len(), 3);
assert_eq!(records[0].actual_id, 27);
assert!((records[0].retention_time_min - 0.0334).abs() < 1e-9);
assert_eq!(records[2].actual_id, 29);
assert!((records[2].retention_time_min - 110.0038).abs() < 1e-9);
}
#[test]
fn empty_payload_returns_no_records() {
let bytes = vec![0u8; HEADER_SIZE];
let path = write_temp("empty.bin", &bytes);
let records = parse(&path).expect("parse should succeed");
let _ = std::fs::remove_file(&path);
assert!(records.is_empty());
}
#[test]
fn file_smaller_than_header_returns_no_records() {
let bytes = vec![0u8; HEADER_SIZE - 1];
let path = write_temp("truncated.bin", &bytes);
let records = parse(&path).expect("parse should succeed");
let _ = std::fs::remove_file(&path);
assert!(records.is_empty());
}
}