use crate::{Result, TelemetryError, VariableInfo, VariableSchema, VariableType};
use std::collections::HashMap;
use tracing::{debug, trace, warn};
const IRSDK_MAX_STRING: usize = 32; const IRSDK_MAX_DESC: usize = 64; const VAR_HEADER_SIZE: usize = std::mem::size_of::<IRSDKVarHeader>();
#[repr(C)]
#[derive(Debug, Clone)]
struct IRSDKVarHeader {
var_type: i32,
offset: i32,
count: i32,
count_as_time: u8,
pad: [u8; 3],
name: [u8; IRSDK_MAX_STRING],
desc: [u8; IRSDK_MAX_DESC],
unit: [u8; IRSDK_MAX_STRING],
}
#[allow(dead_code)]
mod irsdk_var_type {
pub const IRSDK_CHAR: i32 = 0;
pub const IRSDK_BOOL: i32 = 1;
pub const IRSDK_INT: i32 = 2;
pub const IRSDK_BITFIELD: i32 = 3;
pub const IRSDK_FLOAT: i32 = 4;
pub const IRSDK_DOUBLE: i32 = 5;
}
impl IRSDKVarHeader {
pub fn parse_from_memory(memory: &[u8], offset: usize) -> Result<Self> {
trace!(offset, "Parsing variable header from memory");
if offset + VAR_HEADER_SIZE > memory.len() {
return Err(TelemetryError::Memory { offset, source: None });
}
let header = unsafe {
std::ptr::read_unaligned(memory.as_ptr().add(offset) as *const IRSDKVarHeader)
};
header.validate()?;
Ok(header)
}
fn validate(&self) -> Result<()> {
if self.count < 0 {
return Err(TelemetryError::Parse {
context: "Variable header validation".to_string(),
details: format!("Negative element count: {}", self.count),
});
}
if self.count_as_time > 1 {
return Err(TelemetryError::Parse {
context: "Variable header validation".to_string(),
details: format!("Invalid count_as_time flag: {}", self.count_as_time),
});
}
Ok(())
}
fn c_string_to_string(bytes: &[u8]) -> String {
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
String::from_utf8_lossy(&bytes[..end]).to_string()
}
fn map_variable_type(irsdk_type: i32) -> VariableType {
match irsdk_type {
irsdk_var_type::IRSDK_CHAR => VariableType::Char,
irsdk_var_type::IRSDK_BOOL => VariableType::Bool,
irsdk_var_type::IRSDK_INT => VariableType::Int32,
irsdk_var_type::IRSDK_BITFIELD => VariableType::BitField,
irsdk_var_type::IRSDK_FLOAT => VariableType::Float32,
irsdk_var_type::IRSDK_DOUBLE => VariableType::Float64,
_ => {
warn!(irsdk_type, "Unknown iRacing variable type, defaulting to Int32");
VariableType::Int32 }
}
}
pub fn to_variable_info(&self) -> VariableInfo {
VariableInfo {
name: Self::c_string_to_string(&self.name),
data_type: Self::map_variable_type(self.var_type),
offset: self.offset as usize,
count: self.count as usize,
count_as_time: self.count_as_time(),
units: Self::c_string_to_string(&self.unit),
description: Self::c_string_to_string(&self.desc),
}
}
fn count_as_time(&self) -> bool {
self.count_as_time != 0
}
}
pub fn parse_variable_schema(
memory: &[u8],
num_vars: i32,
var_header_offset: i32,
buffer_length: i32,
) -> Result<VariableSchema> {
debug!(num_vars, var_header_offset, buffer_length, "Parsing variable schema from memory");
if num_vars <= 0 {
return Err(TelemetryError::Parse {
context: "Schema parsing".to_string(),
details: format!("Invalid variable count: {}", num_vars),
});
}
if var_header_offset < 0 {
return Err(TelemetryError::Parse {
context: "Schema parsing".to_string(),
details: format!("Invalid variable header offset: {}", var_header_offset),
});
}
let total_headers_size = (num_vars as usize) * VAR_HEADER_SIZE;
let headers_start = var_header_offset as usize;
let headers_end = headers_start + total_headers_size;
if headers_end > memory.len() {
return Err(TelemetryError::Memory { offset: headers_end, source: None });
}
let mut variables = HashMap::with_capacity(num_vars as usize);
let mut failed_count = 0;
for i in 0..num_vars {
let header_offset = headers_start + (i as usize * VAR_HEADER_SIZE);
match IRSDKVarHeader::parse_from_memory(memory, header_offset) {
Ok(var_header) => {
let var_info = var_header.to_variable_info();
if var_info.name.is_empty() || var_info.count == 0 {
continue;
}
if variables.contains_key(&var_info.name) {
warn!(name = %var_info.name, "Duplicate variable name found");
}
variables.insert(var_info.name.clone(), var_info);
}
Err(e) => {
failed_count += 1;
warn!(
error = %e,
header_index = i,
"Failed to parse variable header, skipping"
);
continue;
}
}
}
if failed_count > 0 {
warn!(failed_count, total = num_vars, "Some variable headers failed to parse");
}
debug!(parsed_count = variables.len(), expected_count = num_vars, "Variable parsing completed");
let schema = VariableSchema::new(variables, buffer_length as usize)?;
Ok(schema)
}
#[cfg(all(test, windows))]
mod tests {
use super::*;
use proptest::prelude::*;
use std::mem;
const _: () = assert!(mem::size_of::<IRSDKVarHeader>() == VAR_HEADER_SIZE);
#[test]
fn variable_header_size_matches_expected_layout() {
assert_eq!(mem::size_of::<IRSDKVarHeader>(), 144);
let header = IRSDKVarHeader {
var_type: 0,
offset: 0,
count: 0,
count_as_time: 0,
pad: [0; 3],
name: [0; IRSDK_MAX_STRING],
desc: [0; IRSDK_MAX_DESC],
unit: [0; IRSDK_MAX_STRING],
};
let base_ptr = &header as *const _ as usize;
let type_offset = (&header.var_type as *const _ as usize) - base_ptr;
let offset_offset = (&header.offset as *const _ as usize) - base_ptr;
let count_offset = (&header.count as *const _ as usize) - base_ptr;
let name_offset = (&header.name as *const _ as usize) - base_ptr;
let desc_offset = (&header.desc as *const _ as usize) - base_ptr;
let unit_offset = (&header.unit as *const _ as usize) - base_ptr;
assert_eq!(type_offset, 0);
assert_eq!(offset_offset, 4);
assert_eq!(count_offset, 8);
assert_eq!(name_offset, 16);
assert_eq!(desc_offset, 48);
assert_eq!(unit_offset, 112);
}
#[test]
fn c_string_conversion_works() {
let test_bytes = b"RPM\0\0\0\0";
let result = IRSDKVarHeader::c_string_to_string(test_bytes);
assert_eq!(result, "RPM");
let test_bytes = b"Speed";
let result = IRSDKVarHeader::c_string_to_string(test_bytes);
assert_eq!(result, "Speed");
let test_bytes = b"\0\0\0\0";
let result = IRSDKVarHeader::c_string_to_string(test_bytes);
assert_eq!(result, "");
}
#[test]
fn variable_type_mapping_works() {
assert_eq!(IRSDKVarHeader::map_variable_type(0), VariableType::Char);
assert_eq!(IRSDKVarHeader::map_variable_type(1), VariableType::Bool);
assert_eq!(IRSDKVarHeader::map_variable_type(2), VariableType::Int32);
assert_eq!(IRSDKVarHeader::map_variable_type(3), VariableType::BitField);
assert_eq!(IRSDKVarHeader::map_variable_type(4), VariableType::Float32);
assert_eq!(IRSDKVarHeader::map_variable_type(5), VariableType::Float64);
assert_eq!(IRSDKVarHeader::map_variable_type(99), VariableType::Int32);
}
#[test]
fn insufficient_memory_returns_error() {
let small_memory = vec![0u8; 100]; let result = IRSDKVarHeader::parse_from_memory(&small_memory, 0);
assert!(result.is_err());
}
prop_compose! {
fn arb_valid_var_header()(
name in "[a-zA-Z][a-zA-Z0-9_]*",
desc in "[a-zA-Z0-9 _-]*",
unit in "[a-zA-Z0-9/*^-]*",
var_type in 0..6i32,
offset in 0..100000i32,
count in 1..64i32,
count_as_time in prop::bool::ANY
) -> IRSDKVarHeader {
let mut header = IRSDKVarHeader {
var_type,
offset,
count,
count_as_time: count_as_time as u8,
pad: [0; 3],
name: [0; IRSDK_MAX_STRING],
desc: [0; IRSDK_MAX_DESC],
unit: [0; IRSDK_MAX_STRING],
};
let name_bytes = name.as_bytes();
let len = name_bytes.len().min(IRSDK_MAX_STRING - 1);
header.name[..len].copy_from_slice(&name_bytes[..len]);
let desc_bytes = desc.as_bytes();
let len = desc_bytes.len().min(IRSDK_MAX_DESC - 1);
header.desc[..len].copy_from_slice(&desc_bytes[..len]);
let unit_bytes = unit.as_bytes();
let len = unit_bytes.len().min(IRSDK_MAX_STRING - 1);
header.unit[..len].copy_from_slice(&unit_bytes[..len]);
header
}
}
prop_compose! {
fn arb_corrupted_var_header()(
var_type in (6..100i32).prop_union(-100..0i32),
offset in i32::MIN..0,
count in i32::MIN..0,
count_as_time in 2..=u8::MAX as i32
) -> IRSDKVarHeader {
IRSDKVarHeader {
var_type,
offset,
count,
count_as_time: count_as_time as u8,
pad: [0; 3],
name: [0; IRSDK_MAX_STRING],
desc: [0; IRSDK_MAX_DESC],
unit: [0; IRSDK_MAX_STRING],
}
}
}
proptest! {
#[test]
fn prop_variable_parsing_from_generated_headers(
header in arb_valid_var_header()
) {
let header_bytes = unsafe {
std::slice::from_raw_parts(
&header as *const _ as *const u8,
VAR_HEADER_SIZE
)
};
let parsed = IRSDKVarHeader::parse_from_memory(header_bytes, 0);
prop_assert!(parsed.is_ok());
let var_info = header.to_variable_info();
prop_assert!(!var_info.name.is_empty());
prop_assert!(var_info.count > 0);
}
#[test]
fn prop_corrupted_headers_handled_gracefully(
header in arb_corrupted_var_header()
) {
let header_bytes = unsafe {
std::slice::from_raw_parts(
&header as *const _ as *const u8,
VAR_HEADER_SIZE
)
};
let parsed = IRSDKVarHeader::parse_from_memory(header_bytes, 0);
if let Ok(parsed_header) = parsed {
let var_info = parsed_header.to_variable_info();
let is_known_type = matches!(header.var_type, 0..=5);
if !is_known_type {
prop_assert_eq!(var_info.data_type, VariableType::Int32);
}
}
}
#[test]
fn prop_all_irsdk_types_map_correctly(
irsdk_type in 0..6i32
) {
let mapped_type = IRSDKVarHeader::map_variable_type(irsdk_type);
match irsdk_type {
0 => prop_assert_eq!(mapped_type, VariableType::Char),
1 => prop_assert_eq!(mapped_type, VariableType::Bool),
2 => prop_assert_eq!(mapped_type, VariableType::Int32),
3 => prop_assert_eq!(mapped_type, VariableType::BitField),
4 => prop_assert_eq!(mapped_type, VariableType::Float32),
5 => prop_assert_eq!(mapped_type, VariableType::Float64),
_ => panic!("Invalid irsdk_type {} outside valid range 0-5", irsdk_type),
}
}
#[test]
fn prop_schema_building_with_valid_variables(
var_count in 1..100usize,
buffer_len in 1000..50000i32
) {
let mut memory = Vec::new();
let header_offset = 1000;
memory.resize(header_offset, 0);
for i in 0..var_count {
let header = IRSDKVarHeader {
var_type: 2, offset: (i * 4) as i32, count: 1,
count_as_time: 0,
pad: [0; 3],
name: {
let mut name = [0; IRSDK_MAX_STRING];
let name_str = format!("Var{}", i);
let name_bytes = name_str.as_bytes();
let len = name_bytes.len().min(IRSDK_MAX_STRING - 1);
name[..len].copy_from_slice(&name_bytes[..len]);
name
},
desc: [0; IRSDK_MAX_DESC],
unit: [0; IRSDK_MAX_STRING],
};
let header_bytes = unsafe {
std::slice::from_raw_parts(
&header as *const _ as *const u8,
VAR_HEADER_SIZE
)
};
memory.extend_from_slice(header_bytes);
}
let result = parse_variable_schema(
&memory,
var_count as i32,
header_offset as i32,
buffer_len
);
prop_assert!(result.is_ok());
let schema = result.unwrap();
prop_assert_eq!(schema.variable_count(), var_count);
prop_assert_eq!(schema.frame_size, buffer_len as usize);
}
}
#[test]
#[cfg(feature = "benchmark")]
fn benchmark_variable_schema_parsing_performance() {
use std::time::Instant;
let num_vars = 331;
let var_header_offset = 524400;
let buffer_length = 7817;
let mut memory = vec![0u8; 2_000_000];
for i in 0..num_vars {
let header_offset = var_header_offset + (i * VAR_HEADER_SIZE);
let var_header = IRSDKVarHeader {
var_type: match i % 5 {
0 => 4, 1 => 2, 2 => 1, 3 => 3, 4 => 5, _ => 4, },
offset: (i * 4) as i32, count: if i < 45 {
match i % 3 {
0 => 64, 1 => 6, _ => 1, }
} else {
1
}, count_as_time: if i % 11 == 0 { 1 } else { 0 },
pad: [0; 3],
name: {
let mut name = [0; IRSDK_MAX_STRING];
let name_str = match i % 10 {
0 => "SessionTime",
1 => "RPM",
2 => "Speed",
3 => "Gear",
4 => "CarIdxF2Time",
5 => "LFshockDefl",
6 => "SessionFlags",
7 => "PitsOpen",
8 => "LapDeltaToBestLap",
_ => "TestVariable",
};
let full_name = format!("{}{}", name_str, i);
let name_bytes = full_name.as_bytes();
let len = name_bytes.len().min(IRSDK_MAX_STRING - 1);
name[..len].copy_from_slice(&name_bytes[..len]);
name
},
desc: {
let mut desc = [0; IRSDK_MAX_DESC];
let desc_str = "Test variable description";
let desc_bytes = desc_str.as_bytes();
let len = desc_bytes.len().min(IRSDK_MAX_DESC - 1);
desc[..len].copy_from_slice(&desc_bytes[..len]);
desc
},
unit: {
let mut unit = [0; IRSDK_MAX_STRING];
let unit_str = match i % 4 {
0 => "s", 1 => "rpm", 2 => "m/s", _ => "n/a", };
let unit_bytes = unit_str.as_bytes();
let len = unit_bytes.len().min(IRSDK_MAX_STRING - 1);
unit[..len].copy_from_slice(&unit_bytes[..len]);
unit
},
};
unsafe {
let header_ptr = memory.as_mut_ptr().add(header_offset) as *mut IRSDKVarHeader;
std::ptr::write_unaligned(header_ptr, var_header);
}
}
for _ in 0..10 {
let _ = parse_variable_schema(
&memory,
num_vars as i32,
var_header_offset as i32,
buffer_length,
);
}
const NUM_ITERATIONS: usize = 1000;
let start = Instant::now();
for _ in 0..NUM_ITERATIONS {
let _ = parse_variable_schema(
&memory,
num_vars as i32,
var_header_offset as i32,
buffer_length,
)
.expect("Schema parsing should succeed");
}
let elapsed = start.elapsed();
let avg_duration_nanos = elapsed.as_nanos() as f64 / NUM_ITERATIONS as f64;
let avg_duration_micros = avg_duration_nanos / 1000.0;
println!(
"Variable schema parsing performance: avg {:.2}ns ({:.3}μs) per parse, {} iterations",
avg_duration_nanos, avg_duration_micros, NUM_ITERATIONS
);
assert!(
avg_duration_nanos < 1_000_000.0,
"Variable schema parsing should be <1ms, got {:.2}ns",
avg_duration_nanos
);
assert!(
avg_duration_nanos < 750_000.0, "Variable schema parsing should be <750μs for 60Hz updates, got {:.2}ns",
avg_duration_nanos
);
if avg_duration_nanos < 100_000.0 {
println!(
"✅ Excellent performance: {}x faster than 100μs target",
100_000.0 / avg_duration_nanos
);
} else {
println!("⚠️ Performance acceptable but could be improved");
}
}
}