{"record":{"id":"6b428ce2147603f1","repo":"coding-horror/basic-computer-games","slug":"corrupt-stats-file","errorCode":null,"errorMessage":"Corrupt stats file!","messagePattern":"Corrupt stats file!","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"81_Splat/rust/src/stats.rs","lineNumber":32,"sourceCode":"        if utility::prompt_bool(\"WOULD YOU LIKE TO LOAD PREVIOUS GAME DATA?\", false) {\n            let path = \"src/stats.txt\";\n            let mut altitudes = Vec::new();\n\n            if let Ok(stats) = fs::read_to_string(path) {\n                if stats.is_empty() {\n                    return Some(Stats {\n                        altitudes: Vec::new(),\n                    });\n                }\n\n                let stats: Vec<&str> = stats.trim().split(\",\").collect();\n\n                for s in stats {\n                    if s.is_empty() {\n                        continue;\n                    }\n\n                    let s = s.parse::<f32>().expect(\"Corrupt stats file!\");\n                    altitudes.push(s);\n                }\n\n                return Some(Stats { altitudes });\n            } else {\n                println!(\"PREVIOUS GAME DATA NOT FOUND!\");\n\n                if !utility::prompt_bool(\"WOULD YOU LIKE TO CREATE ONE?\", false) {\n                    return None;\n                } else {\n                    let mut file = File::create(path).expect(\"Invalid file path!\");\n                    file.write_all(\"\".as_bytes())\n                        .expect(\"Could not create file!\");\n\n                    return Some(Stats {\n                        altitudes: Vec::new(),\n                    });\n                }","sourceCodeStart":14,"sourceCodeEnd":50,"githubUrl":"https://github.com/coding-horror/basic-computer-games/blob/5301155192d91d74d337899cecc59dbda59c4c17/81_Splat/rust/src/stats.rs#L14-L50","documentation":"This panic is triggered by `.expect(\"Corrupt stats file!\")` on `s.parse::<f32>()` inside `Stats::new()` in Splat's stats module (line 32). It fires when a comma-separated value in `src/stats.txt` cannot be parsed as an `f32`. The preceding `is_empty()` check filters empty strings (e.g., trailing commas), but any non-numeric token like `\"abc\"`, `\"1.2.3\"`, or `\"NaN_x\"` will panic.","triggerScenarios":"The stats file contains a malformed altitude value. This can happen if the file was hand-edited, corrupted by a partial write, or written by a different version of the program using a different format. Values like `inf`, `nan` are actually valid for `f32::parse` but locale-specific decimal separators (comma vs period) are not.","commonSituations":"Manual editing of `src/stats.txt` introducing non-numeric text. A crash during a previous `write()` leaving a truncated or partial CSV. Locale confusion where the system writes `1,5` (European decimal comma) which then splits into `\"1\"` and `\"5\"` — though in this case each piece parses fine, creating phantom extra entries. File from a different game version with a different schema.","solutions":["Replace `.expect` with `if let Ok(val) = s.parse::<f32>()` and skip/log malformed entries instead of crashing.","Validate or sanitize the stats file format before parsing — e.g., `s.chars().all(|c| c.is_numeric() || c == '.' || c == '-')`.","Back up and reset the stats file when corruption is detected, logging the issue.","Use a more robust serialization format (e.g., serde JSON) to avoid CSV parsing fragility."],"exampleFix":"// before\nlet s = s.parse::<f32>().expect(\"Corrupt stats file!\");\naltitudes.push(s);\n\n// after\nmatch s.parse::<f32>() {\n    Ok(val) => altitudes.push(val),\n    Err(_) => {\n        eprintln!(\"Warning: skipping invalid stat value '{}'\", s);\n        continue;\n    }\n}","handlingStrategy":"validation","validationCode":"// Validate the stats file content before parsing\nfn is_valid_stat(s: &str) -> bool {\n    !s.is_empty() && s.parse::<f32>().is_ok()\n}\n\n// Before loading:\nlet raw = fs::read_to_string(\"src/stats.txt\").unwrap_or_default();\nlet all_valid = raw.trim().split(',').all(|s| s.is_empty() || is_valid_stat(s));\nif !all_valid {\n    eprintln!(\"Stats file is corrupt. Resetting.\");\n    // back up and recreate\n}","typeGuard":"fn parse_stat(s: &str) -> Option<f32> {\n    s.trim().parse::<f32>().ok().filter(|v| v.is_finite())\n}","tryCatchPattern":"for s in stats {\n    if s.is_empty() { continue; }\n    match s.parse::<f32>() {\n        Ok(val) => altitudes.push(val),\n        Err(_) => eprintln!(\"Skipping invalid stat: '{}'\", s),\n    }\n}","preventionTips":["Validate each CSV field with parse::<f32>().is_ok() before using .expect or unwrap.","Back up and reset corrupt stats files instead of crashing.","Consider serde/JSON for stats persistence to avoid CSV parsing fragility.","Filter out non-finite values (NaN, Infinity) if they are not meaningful game data."],"tags":["rust","parse","panic","expect","file-io","csv","stats-persistence","splat"],"backgroundTag":null,"analyzedSha":"5301155192d91d74d337899cecc59dbda59c4c17","analyzedAt":"2026-08-13T19:29:00.979Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}