Skip to main content

glacier_ini/
ini_file.rs

1use glacier_base::encryption::xtea::{Xtea, XteaError};
2use indexmap::IndexMap;
3use itertools::Itertools;
4use std::collections::HashMap;
5use std::io::Write;
6use std::ops::{Index, IndexMut};
7use thiserror::Error;
8
9#[derive(Error, Debug)]
10pub enum IniFileError {
11    #[error("Option ({}) not found", _0)]
12    OptionNotFound(String),
13
14    #[error("Can't find section ({})", _0)]
15    SectionNotFound(String),
16
17    #[error("An error occurred when parsing: {}", _0)]
18    ParsingError(String),
19
20    #[error("An io error occurred: {}", _0)]
21    IoError(#[from] std::io::Error),
22
23    #[error("An io error occurred: {}", _0)]
24    DecryptionError(#[from] XteaError),
25
26    #[error("The given input was incorrect: {}", _0)]
27    InvalidInput(String),
28
29    #[error("The requested include addition already exists: {}", _0)]
30    IncludeAlreadyExists(String),
31}
32
33impl IniFileSection {
34    pub fn new(name: &str) -> Self {
35        Self {
36            name: name.to_string(),
37            options: IndexMap::new(),
38        }
39    }
40
41    pub fn name(&self) -> String {
42        self.name.to_owned()
43    }
44
45    pub fn options(&self) -> &IndexMap<String, String> {
46        &self.options
47    }
48
49    pub fn has_option(&self, option_name: &str) -> bool {
50        self.options.contains_key(option_name)
51    }
52
53    pub fn option(&self, option_name: &str) -> Option<String> {
54        self.options.get(option_name).cloned()
55    }
56
57    pub fn with_option(&mut self, option_name: &str, value: &str) -> &mut Self {
58        self.insert(option_name, value);
59        self
60    }
61
62    pub fn insert(&mut self, option_name: &str, value: &str) {
63        if let Some(key) = self.options.get_mut(option_name) {
64            *key = value.to_string();
65        } else {
66            self.options
67                .insert(option_name.to_string(), value.to_string());
68        }
69    }
70
71    pub fn write_section<W: std::fmt::Write>(&self, writer: &mut W) {
72        writeln!(writer, "[{}]", self.name).unwrap();
73        for (key, value) in &self.options {
74            writeln!(writer, "{key}={value}").unwrap();
75        }
76        writeln!(writer).unwrap();
77    }
78}
79
80#[derive(Default, Debug, Eq, PartialEq)]
81pub struct IniFileSection {
82    pub(crate) name: String,
83    pub(crate) options: IndexMap<String, String>,
84}
85
86/// Represents a system config file for the Glacier engine
87/// ## Example contents
88///
89/// ```txt
90/// [application]
91/// ForceVSync=0
92/// CapWorkerThreads=1
93/// SCENE_FILE=assembly:/path/to/scene.entity
94/// ....
95///
96/// [Hitman5]
97/// usegamecontroller=1
98/// ConsoleCmd UI_EnableMouseEvents 0
99/// ....
100/// ```
101#[derive(Debug, Eq, PartialEq)]
102pub struct IniFile {
103    pub(crate) name: String,
104    pub(crate) description: Option<String>,
105    pub(crate) includes: Vec<IniFile>,
106    pub(crate) sections: HashMap<String, IniFileSection>,
107    pub(crate) console_cmds: Vec<String>,
108}
109
110impl Index<&str> for IniFileSection {
111    type Output = str;
112
113    fn index(&self, option_name: &str) -> &str {
114        self.options.get(option_name).expect("Option not found")
115    }
116}
117
118impl IndexMut<&str> for IniFileSection {
119    fn index_mut(&mut self, option_name: &str) -> &mut str {
120        self.options.entry(option_name.to_string()).or_default()
121    }
122}
123
124impl Default for IniFile {
125    fn default() -> Self {
126        Self {
127            name: "thumbs.dat".to_string(),
128            description: Some(String::from("System config file for the engine")),
129            includes: vec![],
130            sections: Default::default(),
131            console_cmds: vec![],
132        }
133    }
134}
135
136impl IniFile {
137    pub fn new(name: &str) -> Self {
138        Self {
139            name: name.to_string(),
140            description: None,
141            includes: vec![],
142            sections: Default::default(),
143            console_cmds: vec![],
144        }
145    }
146    pub fn name(&self) -> String {
147        self.name.to_string()
148    }
149    pub fn sections(&self) -> &HashMap<String, IniFileSection> {
150        &self.sections
151    }
152
153    pub fn includes(&self) -> &Vec<IniFile> {
154        &self.includes
155    }
156
157    pub fn get_or_add_include(&mut self, include_name: &str) -> &mut IniFile {
158        if self.find_include_mut(include_name).is_none() {
159            self.add_include(IniFile::new(include_name)).unwrap();
160        }
161        self.find_include_mut(include_name).unwrap()
162    }
163
164    pub fn find_include(&self, include_name: &str) -> Option<&IniFile> {
165        self.includes.iter().find(|incl| incl.name == include_name)
166    }
167
168    pub fn find_include_mut(&mut self, include_name: &str) -> Option<&mut IniFile> {
169        self.includes
170            .iter_mut()
171            .find(|incl| incl.name == include_name)
172    }
173
174    pub fn get_option(
175        &self,
176        section_name: &str,
177        option_name: &str,
178    ) -> Result<String, IniFileError> {
179        match self.sections.get(section_name) {
180            Some(v) => match v.options.get(option_name) {
181                Some(o) => Ok(o.clone()),
182                None => Err(IniFileError::OptionNotFound(option_name.to_string())),
183            },
184            None => Err(IniFileError::SectionNotFound(section_name.to_string())),
185        }
186    }
187
188    pub fn set_description(&mut self, description: &str) {
189        self.description = Some(description.to_string())
190    }
191
192    pub fn with_description(&mut self, description: &str) -> &mut Self {
193        self.description = Some(description.to_string());
194        self
195    }
196
197    pub fn with_command(&mut self, command: &str) -> &mut Self {
198        self.console_cmds.push(command.to_string());
199        self
200    }
201
202    pub fn add_section(&mut self, section: IniFileSection) {
203        self.sections.insert(section.name.to_owned(), section);
204    }
205
206    pub fn with_section(&mut self, name: &str) -> &mut IniFileSection {
207        self.add_section(IniFileSection::new(name));
208        self.sections.get_mut(name).unwrap()
209    }
210
211    pub fn add_new_section(&mut self, section_name: &str, values: Option<Vec<(&str, &str)>>) {
212        match self.sections.get_mut(section_name) {
213            None => {
214                self.sections
215                    .insert(section_name.to_string(), IniFileSection::new(section_name));
216                self.add_new_section(section_name, values);
217            }
218            Some(section) => {
219                if let Some(values) = values {
220                    for (key, val) in values {
221                        section.insert(key, val);
222                    }
223                }
224            }
225        }
226    }
227
228    pub fn section(&self, name: &str) -> Option<&IniFileSection> {
229        self.sections.get(name)
230    }
231
232    pub fn section_mut(&mut self, name: &str) -> Option<&mut IniFileSection> {
233        self.sections.get_mut(name)
234    }
235
236    pub fn set_value(
237        &mut self,
238        section_name: &str,
239        option_name: &str,
240        value: &str,
241    ) -> Result<(), IniFileError> {
242        match self.sections.get_mut(section_name) {
243            Some(v) => match v.options.get_mut(option_name) {
244                Some(o) => {
245                    *o = value.to_string();
246                    Ok(())
247                }
248                None => Err(IniFileError::OptionNotFound(option_name.to_string())),
249            },
250            None => Err(IniFileError::SectionNotFound(section_name.to_string())),
251        }
252    }
253
254    pub fn push_console_command(&mut self, command: String) {
255        self.console_cmds.push(command);
256    }
257
258    pub fn add_include(&mut self, include: IniFile) -> Result<(), IniFileError> {
259        if self.includes.contains(&include) {
260            return Err(IniFileError::IncludeAlreadyExists(include.name));
261        }
262        self.includes.push(include);
263        Ok(())
264    }
265
266    pub fn console_cmds(&self) -> &Vec<String> {
267        &self.console_cmds
268    }
269
270    pub fn write_to_file<W: Write>(&self, writer: &mut W, xtea: &Xtea) -> Result<(), IniFileError> {
271        let mut string = String::new();
272        self.write_ini(&mut string);
273        let data = xtea.encrypt_text_file(string)?;
274        writer
275            .write_all(data.as_slice())
276            .map_err(IniFileError::IoError)
277    }
278
279    pub(crate) fn write_ini<W: std::fmt::Write>(&self, writer: &mut W) {
280        if let Some(description) = &self.description {
281            writeln!(writer, "# {description}").unwrap();
282            writeln!(writer, "\n# -----------------------------------------------------------------------------\n", ).unwrap();
283        }
284
285        for section_name in self
286            .sections
287            .keys()
288            .sorted_by(|a, b| Ord::cmp(&a.to_lowercase(), &b.to_lowercase()))
289        {
290            if let Some(section) = self.sections().get(section_name) {
291                section.write_section(writer);
292            }
293        }
294        for console_cmd in &self.console_cmds {
295            writeln!(writer, "ConsoleCmd {console_cmd}").unwrap();
296        }
297        if !self.includes.is_empty() {
298            writeln!(writer).unwrap();
299        }
300        for include in &self.includes {
301            writeln!(writer, "!include {}", include.name).unwrap();
302        }
303    }
304}
305
306impl Index<&str> for IniFile {
307    type Output = IniFileSection;
308
309    fn index(&self, section_name: &str) -> &IniFileSection {
310        self.sections.get(section_name).expect("Section not found")
311    }
312}
313
314impl IndexMut<&str> for IniFile {
315    fn index_mut(&mut self, section_name: &str) -> &mut IniFileSection {
316        self.sections
317            .entry(section_name.to_string())
318            .or_insert(IniFileSection::new(section_name))
319    }
320}