use super::SessionInfo;
use crate::error::TelemetryError;
use anyhow::Result;
use tracing::debug;
#[derive(Debug, Clone)]
pub struct SessionInfoCache {
pub session_info: SessionInfo,
pub version: u32,
pub parsed_at: std::time::SystemTime,
}
impl SessionInfoCache {
pub fn new(session_info: SessionInfo, version: u32) -> Self {
Self { session_info, version, parsed_at: std::time::SystemTime::now() }
}
pub fn is_valid(&self, current_version: u32) -> bool {
self.version == current_version
}
}
#[derive(Debug, Clone)]
pub struct SessionInfoParser {
cache: Option<SessionInfoCache>,
}
impl Default for SessionInfoParser {
fn default() -> Self {
Self::new()
}
}
impl SessionInfoParser {
pub fn new() -> Self {
Self { cache: None }
}
pub fn parse_from_memory(
&mut self,
memory: &[u8],
session_info_offset: i32,
session_info_len: i32,
session_version: u32,
) -> Result<SessionInfo> {
if let Some(cached) = &self.cache {
if cached.is_valid(session_version) {
debug!(version = session_version, "Using cached session info");
return Ok(cached.session_info.clone());
}
}
debug!(
version = session_version,
offset = session_info_offset,
length = session_info_len,
"Parsing fresh session info from memory"
);
let raw_yaml =
self.extract_yaml_from_memory(memory, session_info_offset, session_info_len)?;
let session_info = self.parse(&raw_yaml)?;
self.cache = Some(SessionInfoCache::new(session_info.clone(), session_version));
Ok(session_info)
}
pub fn extract_yaml_from_memory(
&self,
memory: &[u8],
offset: i32,
length: i32,
) -> Result<String> {
if offset < 0 || length <= 0 {
return Err(TelemetryError::Parse {
context: "Session info extraction".to_string(),
details: format!("Invalid offset {} or length {}", offset, length),
}
.into());
}
let start = offset as usize;
let end = start + (length as usize);
if end > memory.len() {
return Err(TelemetryError::Memory { offset: end, source: None }.into());
}
let yaml_bytes = &memory[start..end];
let null_pos = yaml_bytes.iter().position(|&b| b == 0).unwrap_or(yaml_bytes.len());
let yaml_str = String::from_utf8_lossy(&yaml_bytes[..null_pos]).to_string();
if yaml_str.trim().is_empty() {
return Err(TelemetryError::Parse {
context: "Session YAML extraction".to_string(),
details: "Extracted YAML string is empty".to_string(),
}
.into());
}
Ok(yaml_str)
}
pub fn preprocess_iracing_yaml(&self, yaml: &str) -> Result<String> {
const PROBLEMATIC_KEYS: &[&str] = &[
"AbbrevName:",
"TeamName:",
"UserName:",
"Initials:",
"DriverSetupName:",
"CarDesignStr:", ];
let mut cleaned = String::with_capacity(yaml.len());
for ch in yaml.chars() {
if ch.is_control() && ch != '\n' && ch != '\r' && ch != '\t' {
continue;
}
cleaned.push(ch);
}
let lines: Vec<&str> = cleaned.lines().collect();
let mut result_lines = Vec::with_capacity(lines.len());
for line in lines {
let mut processed_line = line.to_string();
for &key in PROBLEMATIC_KEYS {
if let Some(colon_pos) = line.find(key) {
let after_colon = colon_pos + key.len();
if let Some(value_start) =
line[after_colon..].find(|c: char| !c.is_whitespace())
{
let actual_value_start = after_colon + value_start;
let value = line[actual_value_start..].trim();
if !value.is_empty() && !value.starts_with('\'') && !value.starts_with('"')
{
let escaped_value = value.replace('\'', "''");
processed_line = format!(
"{}{} '{}'",
&line[..after_colon],
&line[after_colon..actual_value_start],
escaped_value
);
}
}
break; }
}
result_lines.push(processed_line);
}
let result = result_lines.join("\n");
if yaml.trim().is_empty() {
return Ok(yaml.to_string());
}
Ok(result)
}
pub fn parse(&self, yaml: &str) -> Result<SessionInfo> {
let preprocessed = self.preprocess_iracing_yaml(yaml)?;
match serde_yaml_ng::from_str::<SessionInfo>(&preprocessed) {
Ok(session_info) => {
self.validate_session_info(&session_info)?;
Ok(session_info)
}
Err(e) => Err(TelemetryError::Parse {
context: "Session YAML deserialization".to_string(),
details: format!("YAML parsing failed: {}", e),
}
.into()),
}
}
pub fn validate_session_info(&self, session_info: &SessionInfo) -> Result<()> {
if session_info.weekend_info.track_name.is_empty() {
return Err(TelemetryError::Parse {
context: "Session validation".to_string(),
details: "Missing track name".to_string(),
}
.into());
}
if session_info.weekend_info.track_display_name.is_empty() {
return Err(TelemetryError::Parse {
context: "Session validation".to_string(),
details: "Missing track display name".to_string(),
}
.into());
}
if session_info.session_info.sessions.is_empty() {
return Err(TelemetryError::Parse {
context: "Session validation".to_string(),
details: "No sessions found".to_string(),
}
.into());
}
Ok(())
}
pub fn get_cached(&self, version: u32) -> Option<SessionInfo> {
self.cache
.as_ref()
.filter(|cache| cache.is_valid(version))
.map(|cache| cache.session_info.clone())
}
pub fn clear_cache(&mut self) {
self.cache = None;
}
}