1use crate::encryption::xtea::Xtea;
2use crate::encryption::xtea::XteaError;
3use crate::utils::normalize_path;
4use itertools::Itertools;
5use pathdiff::diff_paths;
6use std::collections::VecDeque;
7use std::io::Write;
8use std::ops::{Index, IndexMut};
9use std::path::PathBuf;
10use std::str::from_utf8;
11use std::{collections::HashMap, fs, path::Path};
12use thiserror::Error;
13
14#[cfg(feature = "serde")]
15use serde::{Deserialize, Serialize};
16
17#[derive(Error, Debug)]
18pub enum IniFileError {
19 #[error("Option ({}) not found", _0)]
20 OptionNotFound(String),
21
22 #[error("Can't find section ({})", _0)]
23 SectionNotFound(String),
24
25 #[error("An error occurred when parsing: {}", _0)]
26 ParsingError(String),
27
28 #[error("An io error occurred: {}", _0)]
29 IoError(#[from] std::io::Error),
30
31 #[error("An io error occurred: {}", _0)]
32 DecryptionError(#[from] XteaError),
33
34 #[error("The given input was incorrect: {}", _0)]
35 InvalidInput(String),
36}
37
38#[derive(Default, Debug)]
39#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
40pub struct IniFileSection {
41 name: String,
42 options: HashMap<String, String>,
43}
44
45#[derive(Debug)]
61#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
62pub struct IniFile {
63 name: String,
64 description: Option<String>,
65 includes: Vec<IniFile>,
66 sections: HashMap<String, IniFileSection>,
67 console_cmds: Vec<String>,
68}
69
70#[derive(Default, Debug)]
90#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
91pub struct IniFileSystem {
92 root: IniFile,
93}
94
95impl IniFileSection {
96 fn new(name: String) -> Self {
97 Self {
98 name,
99 options: HashMap::new(),
100 }
101 }
102
103 pub fn name(&self) -> String {
104 self.name.to_owned()
105 }
106
107 pub fn options(&self) -> &HashMap<String, String> {
108 &self.options
109 }
110
111 pub fn has_option(&self, option_name: &str) -> bool {
112 self.options.contains_key(option_name)
113 }
114
115 fn set_option(&mut self, option_name: &str, value: &str) {
116 if let Some(key) = self.options.get_mut(option_name) {
117 *key = value.to_string();
118 } else {
119 self.options
120 .insert(option_name.to_string(), value.to_string());
121 }
122 }
123
124 pub fn write_section<W: std::fmt::Write>(&self, writer: &mut W) {
125 writeln!(writer, "[{}]", self.name).unwrap();
126 for (key, value) in &self.options {
127 writeln!(writer, "{}={}", key, value).unwrap();
128 }
129 writeln!(writer).unwrap();
130 }
131}
132
133impl Index<&str> for IniFileSection {
134 type Output = str;
135
136 fn index(&self, option_name: &str) -> &str {
137 self.options.get(option_name).expect("Option not found")
138 }
139}
140
141impl IndexMut<&str> for IniFileSection {
142 fn index_mut(&mut self, option_name: &str) -> &mut str {
143 self.options.entry(option_name.to_string()).or_default()
144 }
145}
146
147impl Default for IniFile {
148 fn default() -> Self {
149 Self {
150 name: "thumbs.dat".to_string(),
151 description: Some(String::from("System config file for the engine")),
152 includes: vec![],
153 sections: Default::default(),
154 console_cmds: vec![],
155 }
156 }
157}
158
159impl IniFile {
160 pub fn new(name: &str) -> Self {
161 Self {
162 name: name.to_string(),
163 description: None,
164 includes: vec![],
165 sections: Default::default(),
166 console_cmds: vec![],
167 }
168 }
169 pub fn name(&self) -> String {
170 self.name.to_string()
171 }
172 pub fn sections(&self) -> &HashMap<String, IniFileSection> {
173 &self.sections
174 }
175
176 pub fn includes(&self) -> &Vec<IniFile> {
177 &self.includes
178 }
179
180 pub fn find_include(&self, include_name: &str) -> Option<&IniFile> {
181 self.includes.iter().find(|incl| incl.name == include_name)
182 }
183
184 pub fn get_option(
185 &self,
186 section_name: &str,
187 option_name: &str,
188 ) -> Result<String, IniFileError> {
189 match self.sections.get(section_name) {
190 Some(v) => match v.options.get(option_name.to_uppercase().as_str()) {
191 Some(o) => Ok(o.clone()),
192 None => Err(IniFileError::OptionNotFound(option_name.to_string())),
193 },
194 None => Err(IniFileError::SectionNotFound(section_name.to_string())),
195 }
196 }
197
198 pub fn set_value(
199 &mut self,
200 section_name: &str,
201 option_name: &str,
202 value: &str,
203 ) -> Result<(), IniFileError> {
204 match self.sections.get_mut(section_name) {
205 Some(v) => match v.options.get_mut(option_name) {
206 Some(o) => {
207 *o = value.to_string();
208 Ok(())
209 }
210 None => Err(IniFileError::OptionNotFound(option_name.to_string())),
211 },
212 None => Err(IniFileError::SectionNotFound(section_name.to_string())),
213 }
214 }
215
216 pub fn push_console_command(&mut self, command: String) {
217 self.console_cmds.push(command);
218 }
219
220 pub fn console_cmds(&self) -> &Vec<String> {
221 &self.console_cmds
222 }
223
224 pub fn write_ini_file<W: std::fmt::Write>(&self, writer: &mut W) {
225 if let Some(description) = &self.description {
226 writeln!(writer, "# {}", description).unwrap();
227 writeln!(writer, "\n# -----------------------------------------------------------------------------\n", ).unwrap();
228 }
229 for include in &self.includes {
230 writeln!(writer, "!include {}", include.name).unwrap();
231 }
232 for section_name in self
233 .sections
234 .keys()
235 .sorted_by(|a, b| Ord::cmp(&a.to_lowercase(), &b.to_lowercase()))
236 {
237 if let Some(section) = self.sections().get(section_name) {
238 section.write_section(writer);
239 }
240 }
241 for console_cmd in &self.console_cmds {
242 writeln!(writer, "ConsoleCmd {}", console_cmd).unwrap();
243 }
244 }
245}
246
247impl IniFileSystem {
248 pub fn new() -> Self {
249 Self {
250 root: IniFile::new("thumbs.dat"),
251 }
252 }
253
254 pub fn load(&mut self, root_file: impl AsRef<Path>) -> Result<(), IniFileError> {
256 let ini_file = Self::load_from_path(
257 root_file.as_ref(),
258 PathBuf::from(root_file.as_ref()).parent().unwrap(),
259 )?;
260 self.root = ini_file;
261 Ok(())
262 }
263
264 pub fn from(root_file: impl AsRef<Path>) -> Result<Self, IniFileError> {
265 let mut ret = Self::new();
266 match ret.load(root_file) {
267 Ok(_) => Ok(ret),
268 Err(e) => Err(e),
269 }
270 }
271
272 fn load_from_path(path: &Path, working_directory: &Path) -> Result<IniFile, IniFileError> {
273 let content = fs::read(path).map_err(IniFileError::IoError)?;
274 let mut content_decrypted = from_utf8(content.as_ref()).unwrap_or("").to_string();
275 if Xtea::is_encrypted_text_file(&content) {
276 content_decrypted =
277 Xtea::decrypt_text_file(&content).map_err(IniFileError::DecryptionError)?;
278 }
279
280 let ini_file_name = match diff_paths(path, working_directory) {
281 Some(relative_path) => relative_path.to_str().unwrap().to_string(),
282 None => path.to_str().unwrap().to_string(),
283 };
284 Self::load_from_string(
285 ini_file_name.as_str(),
286 content_decrypted.as_str(),
287 working_directory,
288 )
289 }
290
291 fn load_from_string(
292 name: &str,
293 ini_file_content: &str,
294 working_directory: &Path,
295 ) -> Result<IniFile, IniFileError> {
296 let mut active_section: String = "None".to_string();
297 let mut ini_file = IniFile::new(name);
298
299 for line in ini_file_content.lines() {
300 if let Some(description) = line.strip_prefix('#') {
301 if ini_file_content.starts_with(line) {
302 ini_file.description = Some(description.trim_start().to_string());
304 }
305 } else if let Some(line) = line.strip_prefix('!') {
306 if let Some((command, value)) = line.split_once(' ') {
307 if command == "include" {
308 let include = Self::load_from_path(
309 working_directory.join(value).as_path(),
310 working_directory,
311 )?;
312 ini_file.includes.push(include);
313 }
314 }
315 } else if let Some(mut section_name) = line.strip_prefix('[') {
316 section_name = section_name
317 .strip_suffix(']')
318 .ok_or(IniFileError::ParsingError(
319 "a section should always have a closing ] bracket".to_string(),
320 ))?;
321 active_section = section_name.to_string();
322 if !ini_file.sections.contains_key(&active_section) {
323 ini_file.sections.insert(
324 active_section.clone(),
325 IniFileSection::new(active_section.clone()),
326 );
327 }
328 } else if let Some(keyval) = line.strip_prefix("ConsoleCmd ") {
329 ini_file.console_cmds.push(keyval.to_string());
330 } else if let Some((key, val)) = line.split_once('=') {
331 if let Some(section) = ini_file.sections.get_mut(&active_section) {
332 section.set_option(key.to_uppercase().as_str(), val);
333 }
334 }
335 }
336 Ok(ini_file)
337 }
338
339 pub fn write_to_folder<P: AsRef<Path>>(&self, path: P) -> Result<(), IniFileError> {
340 let mut folder = path.as_ref();
341 if folder.is_file() {
342 folder = path.as_ref().parent().ok_or(IniFileError::InvalidInput(
343 "The export path cannot be empty".to_string(),
344 ))?;
345 }
346 fn write_children_to_folder(path: &Path, ini_file: &IniFile) -> Result<(), IniFileError> {
347 let mut file_path = path.join(&ini_file.name);
348 file_path = normalize_path(&file_path);
349
350 let parent_dir = file_path.parent().ok_or(IniFileError::InvalidInput(
351 "Invalid export path given".to_string(),
352 ))?;
353 fs::create_dir_all(parent_dir)?;
354
355 let mut writer = fs::OpenOptions::new()
356 .write(true)
357 .create(true)
358 .truncate(true)
359 .open(&file_path)?;
360 let mut contents = String::new();
361 ini_file.write_ini_file(&mut contents);
362 let _ = writer.write_all(contents.as_bytes());
363
364 for include in ini_file.includes.iter() {
365 match write_children_to_folder(parent_dir, include) {
366 Ok(_) => {}
367 Err(e) => return Err(e),
368 };
369 }
370 Ok(())
371 }
372
373 write_children_to_folder(folder, &self.root)
374 }
375
376 pub fn normalize(&mut self) {
378 let mut queue: VecDeque<IniFile> = VecDeque::new();
379 for include in self.root.includes.drain(0..) {
380 queue.push_back(include);
381 }
382
383 while let Some(mut current_file) = queue.pop_front() {
384 let root_sections = &mut self.root.sections;
385
386 for (section_key, section) in current_file.sections.drain() {
387 if !root_sections.contains_key(§ion_key) {
388 root_sections.insert(section_key.clone(), section);
389 } else {
390 let root_section = root_sections.get_mut(§ion_key).unwrap();
391 for (key, value) in section.options {
392 if !root_section.has_option(&key) {
393 root_section.set_option(&key, &value);
394 } else {
395 root_section.set_option(&key, value.as_str());
396 }
397 }
398 }
399 }
400
401 for console_cmd in current_file.console_cmds.drain(..) {
402 if !self.root.console_cmds.contains(&console_cmd) {
403 self.root.console_cmds.push(console_cmd);
404 }
405 }
406 for include in current_file.includes.drain(0..) {
407 queue.push_back(include);
408 }
409 }
410 }
411
412 pub fn console_cmds(&self) -> Vec<String> {
414 let mut cmds: Vec<String> = vec![];
415
416 fn traverse_includes(ini_file: &IniFile, cmds: &mut Vec<String>) {
418 for include in &ini_file.includes {
419 cmds.extend_from_slice(&include.console_cmds);
420 traverse_includes(include, cmds);
421 }
422 }
423
424 cmds.extend_from_slice(&self.root.console_cmds);
425 traverse_includes(&self.root, &mut cmds);
426
427 cmds
428 }
429
430 pub fn option(&self, section_name: &str, option_name: &str) -> Result<String, IniFileError> {
432 let mut queue: VecDeque<&IniFile> = VecDeque::new();
433 queue.push_back(&self.root);
434 let mut latest_value: Option<String> = None;
435
436 while let Some(current_file) = queue.pop_front() {
437 if let Ok(value) = current_file.get_option(section_name, option_name) {
438 latest_value = Some(value.clone());
440 }
441 for include in ¤t_file.includes {
442 queue.push_back(include);
443 }
444 }
445
446 latest_value.ok_or_else(|| IniFileError::OptionNotFound(option_name.to_string()))
448 }
449
450 pub fn root(&self) -> &IniFile {
452 &self.root
453 }
454}
455
456impl Index<&str> for IniFile {
457 type Output = IniFileSection;
458
459 fn index(&self, section_name: &str) -> &IniFileSection {
460 self.sections.get(section_name).expect("Section not found")
461 }
462}
463
464impl IndexMut<&str> for IniFile {
465 fn index_mut(&mut self, section_name: &str) -> &mut IniFileSection {
466 self.sections
467 .entry(section_name.to_string())
468 .or_insert(IniFileSection::new(section_name.to_string()))
469 }
470}