use std::collections::HashMap;
use std::path::Path;
use crate::error::Result;
pub struct Listfile {
by_id: HashMap<u32, String>,
by_path: HashMap<String, u32>,
}
impl Listfile {
pub fn parse(content: &str) -> Self {
let mut by_id = HashMap::new();
let mut by_path = HashMap::new();
for line in content.lines() {
let line = line.trim_end_matches('\r');
if line.is_empty() {
continue;
}
let Some((id_str, path)) = line.split_once(';') else {
continue;
};
let Ok(fdid) = id_str.parse::<u32>() else {
continue;
};
if path.is_empty() {
continue;
}
by_path.insert(path.to_lowercase(), fdid);
by_id.insert(fdid, path.to_string());
}
Self { by_id, by_path }
}
pub fn load(path: &Path) -> Result<Self> {
let content = std::fs::read_to_string(path)?;
Ok(Self::parse(&content))
}
pub fn path(&self, fdid: u32) -> Option<&str> {
self.by_id.get(&fdid).map(|s| s.as_str())
}
pub fn fdid(&self, path: &str) -> Option<u32> {
self.by_path.get(&path.to_lowercase()).copied()
}
pub fn len(&self) -> usize {
self.by_id.len()
}
pub fn is_empty(&self) -> bool {
self.by_id.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_basic() {
let content = "53;Cameras/FlyBy.m2\n69;Creature/Bear/bear.m2\n";
let lf = Listfile::parse(content);
assert_eq!(lf.len(), 2);
assert_eq!(lf.path(53), Some("Cameras/FlyBy.m2"));
assert_eq!(lf.path(69), Some("Creature/Bear/bear.m2"));
}
#[test]
fn parse_reverse_lookup() {
let content = "53;Cameras/FlyBy.m2\n";
let lf = Listfile::parse(content);
assert_eq!(lf.fdid("cameras/flyby.m2"), Some(53)); }
#[test]
fn parse_skips_malformed() {
let content = "53;Valid/Path.m2\nnot_a_number;Bad\n;empty_id\n42\n\n";
let lf = Listfile::parse(content);
assert_eq!(lf.len(), 1);
assert_eq!(lf.path(53), Some("Valid/Path.m2"));
}
#[test]
fn parse_empty() {
let lf = Listfile::parse("");
assert!(lf.is_empty());
}
#[test]
fn lookup_miss() {
let lf = Listfile::parse("53;Test.m2\n");
assert!(lf.path(999).is_none());
assert!(lf.fdid("nonexistent").is_none());
}
#[test]
fn parse_handles_windows_line_endings() {
let content = "53;Path/A.m2\r\n69;Path/B.m2\r\n";
let lf = Listfile::parse(content);
assert_eq!(lf.len(), 2);
assert_eq!(lf.path(53), Some("Path/A.m2"));
}
#[test]
fn parse_preserves_original_case_in_path() {
let content = "53;World/Maps/Azeroth.wdt\n";
let lf = Listfile::parse(content);
assert_eq!(lf.path(53), Some("World/Maps/Azeroth.wdt"));
assert_eq!(lf.fdid("world/maps/azeroth.wdt"), Some(53));
}
}