1use crate::{ByteBuffer, ByteSpan};
5use std::collections::HashMap;
6use std::io::{BufRead, BufReader, BufWriter, Cursor, Write};
7
8#[derive(Debug)]
10pub struct ConfigMap {
11 pub keys: Vec<(String, String)>,
13}
14
15#[derive(Debug)]
17pub struct ConfigFile {
18 pub categories: Vec<String>,
20 pub settings: HashMap<String, ConfigMap>,
22}
23
24impl ConfigFile {
25 pub fn from_existing(buffer: ByteSpan) -> Option<ConfigFile> {
27 let mut cfg = ConfigFile {
28 categories: Vec::new(),
29 settings: HashMap::new(),
30 };
31
32 let cursor = Cursor::new(buffer);
33 let reader = BufReader::new(cursor);
34
35 let mut current_category: Option<String> = None;
36
37 for line in reader.lines().map_while(Result::ok) {
38 if !line.is_empty() && line != "\0" {
39 if line.contains('<') || line.contains('>') {
40 let name = &line[1..line.len() - 1];
42 current_category = Some(String::from(name));
43 cfg.categories.push(String::from(name));
44 } else if let (Some(category), Some((key, value))) =
45 (¤t_category, line.split_once('\t'))
46 {
47 cfg.settings
49 .entry(category.clone())
50 .or_insert_with(|| ConfigMap { keys: Vec::new() });
51 cfg.settings
52 .get_mut(category)?
53 .keys
54 .push((key.to_string(), value.to_string()));
55 }
56 }
57 }
58
59 Some(cfg)
60 }
61
62 pub fn write_to_buffer(&self) -> Option<ByteBuffer> {
64 let mut buffer = ByteBuffer::new();
65
66 {
67 let cursor = Cursor::new(&mut buffer);
68 let mut writer = BufWriter::new(cursor);
69
70 for category in &self.categories {
71 writer
72 .write_all(format!("\r\n<{}>\r\n", category).as_ref())
73 .ok()?;
74
75 if self.settings.contains_key(category) {
76 for key in &self.settings[category].keys {
77 writer
78 .write_all(format!("{}\t{}\r\n", key.0, key.1).as_ref())
79 .ok()?;
80 }
81 }
82 }
83
84 writer.write_all(b"\0").ok()?;
85 }
86
87 Some(buffer)
88 }
89
90 pub fn has_key(&self, select_key: &str) -> bool {
92 for map in self.settings.values() {
93 for (key, _) in &map.keys {
94 if select_key == key {
95 return true;
96 }
97 }
98 }
99
100 false
101 }
102
103 pub fn has_category(&self, select_category: &str) -> bool {
105 self.settings.contains_key(select_category)
106 }
107
108 pub fn set_value(&mut self, select_key: &str, new_value: &str) {
110 for keys in self.settings.values_mut() {
111 for (key, value) in &mut keys.keys {
112 if select_key == key {
113 *value = new_value.to_string();
114 }
115 }
116 }
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use std::fs::read;
123 use std::path::PathBuf;
124
125 use super::*;
126
127 fn common_setup() -> ConfigFile {
128 let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
129 d.push("resources/tests");
130 d.push("FFXIV.cfg");
131
132 ConfigFile::from_existing(&read(d).unwrap()).unwrap()
133 }
134
135 fn common_setup_modified() -> ByteBuffer {
136 let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
137 d.push("resources/tests");
138 d.push("FFXIV.modified.cfg");
139
140 read(d).unwrap()
141 }
142
143 fn common_setup_invalid() -> ByteBuffer {
144 let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
145 d.push("resources/tests");
146 d.push("random");
147
148 read(d).unwrap()
149 }
150
151 #[test]
152 fn basic_parsing() {
153 let cfg = common_setup();
154
155 assert!(cfg.has_key("TextureFilterQuality"));
156 assert!(cfg.has_category("Cutscene Settings"));
157 }
158
159 #[test]
160 fn basic_writing() {
161 let mut cfg = common_setup();
162 let modified_cfg = common_setup_modified();
163
164 cfg.set_value("CutsceneMovieOpening", "1");
165
166 let cfg_buffer = cfg.write_to_buffer().unwrap();
167
168 assert_eq!(modified_cfg, cfg_buffer);
169 }
170
171 #[test]
172 fn test_invalid() {
173 let cfg = common_setup_invalid();
174
175 ConfigFile::from_existing(&cfg);
177 }
178}