1use crate::ini_file::{IniFile, IniFileError, IniFileSection};
2use crate::utils::normalize_path;
3use glacier_base::encryption::xtea::{Xtea, XteaConfig};
4use pathdiff::diff_paths;
5use std::collections::VecDeque;
6use std::fs;
7use std::path::{Path, PathBuf};
8use std::str::from_utf8;
9
10pub mod ini_file;
11mod utils;
12
13pub struct IniKey {
15 pub(crate) section: String,
16 pub(crate) option: String,
17}
18
19impl IniKey {
20 pub fn from_tuple(section: &str, option: &str) -> Self {
21 Self {
22 section: section.to_string(),
23 option: option.to_string(),
24 }
25 }
26
27 pub fn from_location(path: &str) -> Self {
33 match path.split_once("/") {
34 Some((section, option)) => Self {
35 section: section.to_string(),
36 option: option.to_string(),
37 },
38 None => Self {
39 section: "".to_string(),
40 option: path.to_string(),
41 },
42 }
43 }
44}
45
46impl From<&str> for IniKey {
47 fn from(path: &str) -> Self {
48 IniKey::from_location(path)
49 }
50}
51
52impl From<(&str, &str)> for IniKey {
53 fn from(tuple: (&str, &str)) -> Self {
54 IniKey::from_tuple(tuple.0, tuple.1)
55 }
56}
57
58#[derive(Debug)]
80pub struct IniFileSystem {
81 root: IniFile,
82}
83
84impl IniFileSystem {
85 pub fn new(ini_file: IniFile) -> Self {
86 Self { root: ini_file }
87 }
88
89 pub fn from_path(
95 root_file: impl AsRef<Path>,
96 xtea_config: XteaConfig,
97 ) -> Result<Self, IniFileError> {
98 let ini_file = Self::load_from_path(
99 root_file.as_ref(),
100 PathBuf::from(root_file.as_ref()).parent().unwrap(),
101 &Xtea::new(xtea_config),
102 )?;
103 Ok(Self { root: ini_file })
104 }
105
106 fn load_from_path(
107 path: &Path,
108 working_directory: &Path,
109 xtea: &Xtea,
110 ) -> Result<IniFile, IniFileError> {
111 let content = fs::read(path).map_err(IniFileError::IoError)?;
112 let mut content_decrypted = from_utf8(content.as_ref()).unwrap_or("").to_string();
113 if xtea.is_encrypted_text_file(&content) {
114 content_decrypted = xtea
115 .decrypt_text_file(&content)
116 .map_err(IniFileError::DecryptionError)?;
117 }
118
119 let ini_file_name = match diff_paths(path, working_directory) {
120 Some(relative_path) => relative_path.to_str().unwrap().to_string(),
121 None => path.to_str().unwrap().to_string(),
122 };
123 Self::load_from_string(
124 ini_file_name.as_str(),
125 content_decrypted.as_str(),
126 working_directory,
127 xtea,
128 )
129 }
130
131 fn load_from_string(
132 name: &str,
133 ini_file_content: &str,
134 working_directory: &Path,
135 xtea: &Xtea,
136 ) -> Result<IniFile, IniFileError> {
137 let mut active_section: String = "None".to_string();
138 let mut ini_file = IniFile::new(name);
139
140 for line in ini_file_content.lines() {
141 if let Some(description) = line.strip_prefix('#') {
142 if ini_file_content.starts_with(line) {
143 ini_file.description = Some(description.trim_start().to_string());
145 }
146 } else if let Some(line) = line.strip_prefix('!') {
147 if let Some((command, value)) = line.split_once(' ') {
148 if command == "include" {
149 let include = Self::load_from_path(
150 working_directory.join(value).as_path(),
151 working_directory,
152 xtea,
153 )?;
154 ini_file.includes.push(include);
155 }
156 }
157 } else if let Some(mut section_name) = line.strip_prefix('[') {
158 section_name = section_name
159 .strip_suffix(']')
160 .ok_or(IniFileError::ParsingError(
161 "a section should always have a closing ] bracket".to_string(),
162 ))?;
163 active_section = section_name.to_string();
164 if !ini_file.sections.contains_key(&active_section) {
165 ini_file.sections.insert(
166 active_section.clone(),
167 IniFileSection::new(&active_section.clone()),
168 );
169 }
170 } else if let Some(keyval) = line.strip_prefix("ConsoleCmd ") {
171 ini_file.console_cmds.push(keyval.to_string());
172 } else if let Some((key, val)) = line.split_once('=') {
173 if let Some(section) = ini_file.sections.get_mut(&active_section) {
174 section.insert(key, val);
175 }
176 }
177 }
178 Ok(ini_file)
179 }
180
181 pub fn write_to_folder<P: AsRef<Path>>(
190 &self,
191 path: P,
192 xtea_config: XteaConfig,
193 ) -> Result<(), IniFileError> {
194 let mut folder = path.as_ref();
195 if folder.is_file() {
196 folder = path.as_ref().parent().ok_or(IniFileError::InvalidInput(
197 "The export path cannot be empty".to_string(),
198 ))?;
199 }
200 fn write_children_to_folder(
201 path: &Path,
202 ini_file: &IniFile,
203 xtea: &Xtea,
204 ) -> Result<(), IniFileError> {
205 let mut file_path = path.join(&ini_file.name);
206 file_path = normalize_path(&file_path);
207
208 let parent_dir = file_path.parent().ok_or(IniFileError::InvalidInput(
209 "Invalid export path given".to_string(),
210 ))?;
211 fs::create_dir_all(parent_dir)?;
212
213 let mut writer = fs::OpenOptions::new()
214 .write(true)
215 .create(true)
216 .truncate(true)
217 .open(&file_path)?;
218 ini_file.write_to_file(&mut writer, xtea)?;
219
220 for include in ini_file.includes.iter() {
221 match write_children_to_folder(parent_dir, include, xtea) {
222 Ok(_) => {}
223 Err(e) => return Err(e),
224 };
225 }
226 Ok(())
227 }
228
229 write_children_to_folder(folder, &self.root, &Xtea::new(xtea_config))
230 }
231
232 pub fn normalize(&mut self) {
238 let mut queue: VecDeque<IniFile> = VecDeque::new();
239 for include in self.root.includes.drain(0..) {
240 queue.push_back(include);
241 }
242
243 while let Some(mut current_file) = queue.pop_front() {
244 let root_sections = &mut self.root.sections;
245
246 for (section_key, section) in current_file.sections.drain() {
247 if !root_sections.contains_key(§ion_key) {
248 root_sections.insert(section_key.clone(), section);
249 } else {
250 let root_section = root_sections.get_mut(§ion_key).unwrap();
251 for (key, value) in section.options {
252 if !root_section.has_option(&key) {
253 root_section.insert(&key, &value);
254 } else {
255 root_section.insert(&key, value.as_str());
256 }
257 }
258 }
259 }
260
261 for console_cmd in current_file.console_cmds.drain(..) {
262 if !self.root.console_cmds.contains(&console_cmd) {
263 self.root.console_cmds.push(console_cmd);
264 }
265 }
266 for include in current_file.includes.drain(0..) {
267 queue.push_back(include);
268 }
269 }
270 }
271
272 pub fn console_cmds(&self) -> Vec<String> {
274 let mut cmds: Vec<String> = vec![];
275
276 fn traverse_includes(ini_file: &IniFile, cmds: &mut Vec<String>) {
278 for include in &ini_file.includes {
279 cmds.extend_from_slice(&include.console_cmds);
280 traverse_includes(include, cmds);
281 }
282 }
283
284 cmds.extend_from_slice(&self.root.console_cmds);
285 traverse_includes(&self.root, &mut cmds);
286
287 cmds
288 }
289
290 pub fn option(&self, key: impl Into<IniKey> + Clone) -> Result<String, IniFileError> {
292 let mut queue: VecDeque<&IniFile> = VecDeque::new();
293 queue.push_back(&self.root);
294 let mut latest_value: Option<String> = None;
295
296 while let Some(current_file) = queue.pop_front() {
297 if let Ok(value) =
298 current_file.get_option(&key.clone().into().section, &key.clone().into().option)
299 {
300 latest_value = Some(value.clone());
302 }
303 for include in ¤t_file.includes {
304 queue.push_back(include);
305 }
306 }
307
308 latest_value
310 .ok_or_else(|| IniFileError::OptionNotFound(key.clone().into().option.to_string()))
311 }
312
313 pub fn root(&self) -> &IniFile {
315 &self.root
316 }
317
318 pub fn root_mut(&mut self) -> &mut IniFile {
320 &mut self.root
321 }
322}