libsubconverter/utils/
ini_reader.rs

1//! INI file reader implementation
2//!
3//! This module provides functionality for reading and parsing INI files,
4//! similar to the C++ INIReader class in the original subconverter.
5
6use std::collections::{HashMap, HashSet};
7use std::fs::File;
8use std::io::{self, Read};
9use std::path::Path;
10
11use super::{file_exists, file_get_async};
12
13/// Error types for the INI reader
14#[derive(Debug)]
15pub enum IniReaderError {
16    Empty,
17    Duplicate,
18    OutOfBound,
19    NotExist,
20    NotParsed,
21    IoError(io::Error),
22    None,
23}
24
25impl std::fmt::Display for IniReaderError {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        match self {
28            IniReaderError::Empty => write!(f, "Empty document"),
29            IniReaderError::Duplicate => write!(f, "Duplicate section"),
30            IniReaderError::OutOfBound => write!(f, "Item exists outside of any section"),
31            IniReaderError::NotExist => write!(f, "Target does not exist"),
32            IniReaderError::NotParsed => write!(f, "Parse error"),
33            IniReaderError::IoError(e) => write!(f, "IO error: {}", e),
34            IniReaderError::None => write!(f, "No error"),
35        }
36    }
37}
38
39impl From<io::Error> for IniReaderError {
40    fn from(error: io::Error) -> Self {
41        IniReaderError::IoError(error)
42    }
43}
44
45/// Custom INI reader implementation similar to C++ INIReader
46pub struct IniReader {
47    /// The parsed INI content (sections -> [(key, value)])
48    content: HashMap<String, Vec<(String, String)>>,
49    /// Whether the INI has been successfully parsed
50    parsed: bool,
51    /// The current section being operated on
52    current_section: String,
53    /// List of sections to exclude when parsing
54    exclude_sections: HashSet<String>,
55    /// List of sections to include when parsing (if empty, all sections are
56    /// included)
57    include_sections: HashSet<String>,
58    /// List of sections to save directly without processing
59    direct_save_sections: HashSet<String>,
60    /// Ordered list of sections as they appear in the original file
61    section_order: Vec<String>,
62    /// Last error that occurred
63    last_error: IniReaderError,
64    /// Save any line within a section even if it doesn't follow the key=value
65    /// format
66    pub store_any_line: bool,
67    /// Allow section titles to appear multiple times
68    pub allow_dup_section_titles: bool,
69    /// Keep empty sections while parsing
70    pub keep_empty_section: bool,
71    /// For storing lines before any section is defined
72    isolated_items_section: String,
73    /// Store isolated lines (lines before any section)
74    pub store_isolated_line: bool,
75}
76
77impl Default for IniReader {
78    fn default() -> Self {
79        Self::new()
80    }
81}
82
83impl IniReader {
84    /// Create a new INI reader
85    pub fn new() -> Self {
86        IniReader {
87            content: HashMap::new(),
88            parsed: false,
89            current_section: String::new(),
90            exclude_sections: HashSet::new(),
91            include_sections: HashSet::new(),
92            direct_save_sections: HashSet::new(),
93            section_order: Vec::new(),
94            last_error: IniReaderError::None,
95            store_any_line: false,
96            allow_dup_section_titles: false,
97            keep_empty_section: true,
98            isolated_items_section: String::new(),
99            store_isolated_line: false,
100        }
101    }
102
103    /// Add a section to be excluded during parsing
104    pub fn exclude_section(&mut self, section: &str) {
105        self.exclude_sections.insert(section.to_string());
106    }
107
108    /// Add a section to be included during parsing
109    pub fn include_section(&mut self, section: &str) {
110        self.include_sections.insert(section.to_string());
111    }
112
113    /// Add a section to be saved directly without processing
114    pub fn add_direct_save_section(&mut self, section: &str) {
115        self.direct_save_sections.insert(section.to_string());
116    }
117
118    /// Set the section to store isolated items
119    pub fn set_isolated_items_section(&mut self, section: &str) {
120        self.isolated_items_section = section.to_string();
121    }
122
123    /// Erase all contents of the current section (keeps the section, just
124    /// empties it)
125    pub fn erase_section(&mut self) {
126        if self.current_section.is_empty() {
127            return;
128        }
129
130        if let Some(section_vec) = self.content.get_mut(&self.current_section) {
131            section_vec.clear();
132        }
133    }
134
135    /// Erase all items in a specific section but keep the section itself
136    pub fn erase_section_by_name(&mut self, section: &str) {
137        if !self.section_exist(section) {
138            return;
139        }
140
141        if let Some(section_vec) = self.content.get_mut(section) {
142            section_vec.clear();
143        }
144    }
145
146    /// Check if a section should be ignored based on include/exclude settings
147    fn should_ignore_section(&self, section: &str) -> bool {
148        let excluded = self.exclude_sections.contains(section);
149        let included = if self.include_sections.is_empty() {
150            true
151        } else {
152            self.include_sections.contains(section)
153        };
154
155        excluded || !included
156    }
157
158    /// Check if a section should be saved directly without processing
159    fn should_direct_save(&self, section: &str) -> bool {
160        self.direct_save_sections.contains(section)
161    }
162
163    /// Create a new INI reader and parse a file
164    pub async fn from_file(path: &str) -> Result<Self, IniReaderError> {
165        let mut reader = IniReader::new();
166        reader.parse_file(path).await?;
167        Ok(reader)
168    }
169
170    /// Get the last error as a string
171    pub fn get_last_error(&self) -> String {
172        self.last_error.to_string()
173    }
174
175    /// Trim whitespace from a string
176    fn trim_whitespace(s: &str) -> String {
177        s.trim().to_string()
178    }
179
180    /// Process escape characters in a string
181    fn process_escape_char(s: &mut String) {
182        // Replace escape sequences with actual characters
183        *s = s
184            .replace("\\n", "\n")
185            .replace("\\r", "\r")
186            .replace("\\t", "\t");
187    }
188
189    /// Process escape characters in reverse (for writing)
190    fn process_escape_char_reverse(s: &mut String) {
191        // Replace actual characters with escape sequences
192        *s = s
193            .replace('\n', "\\n")
194            .replace('\r', "\\r")
195            .replace('\t', "\\t");
196    }
197
198    /// Erase all data from the INI
199    pub fn erase_all(&mut self) {
200        self.content.clear();
201        self.section_order.clear();
202        self.current_section.clear();
203        self.parsed = false;
204    }
205
206    /// Check if parsed successfully
207    pub fn is_parsed(&self) -> bool {
208        self.parsed
209    }
210
211    /// Parse INI content into the internal data structure
212    pub fn parse(&mut self, content: &str) -> Result<(), IniReaderError> {
213        // First clear all data
214        self.erase_all();
215
216        if content.is_empty() {
217            self.last_error = IniReaderError::Empty;
218            return Err(IniReaderError::Empty);
219        }
220
221        // Remove UTF-8 BOM if present
222        let content = if content.starts_with("\u{FEFF}") {
223            &content[3..]
224        } else {
225            content
226        };
227
228        let mut in_excluded_section = false;
229        let mut in_direct_save_section = false;
230        let mut in_isolated_section = false;
231
232        let mut cur_section = String::new();
233        let mut item_group = Vec::new();
234        let mut read_sections = Vec::new();
235
236        // Check if we need to handle isolated items
237        if self.store_isolated_line && !self.isolated_items_section.is_empty() {
238            cur_section = self.isolated_items_section.clone();
239            in_excluded_section = self.should_ignore_section(&cur_section);
240            in_direct_save_section = self.should_direct_save(&cur_section);
241            in_isolated_section = true;
242        }
243
244        // Process each line
245        for line in content.lines() {
246            let line = Self::trim_whitespace(line);
247
248            // Skip empty lines and comments
249            if line.is_empty()
250                || line.starts_with(';')
251                || line.starts_with('#')
252                || line.starts_with("//")
253            {
254                continue;
255            }
256
257            let mut line = line.to_string();
258
259            // Process escape characters
260            Self::process_escape_char(&mut line);
261
262            // Check if it's a section header [section]
263            if line.starts_with('[') && line.ends_with(']') && line.len() >= 3 {
264                let this_section = line[1..line.len() - 1].to_string();
265                in_excluded_section = self.should_ignore_section(&this_section);
266                in_direct_save_section = self.should_direct_save(&this_section);
267
268                // Save previous section if not empty
269                if !cur_section.is_empty() && (self.keep_empty_section || !item_group.is_empty()) {
270                    if self.content.contains_key(&cur_section) {
271                        // Handle duplicate section
272                        if self.allow_dup_section_titles
273                            || self.content.get(&cur_section).unwrap().is_empty()
274                        {
275                            // Merge with existing section
276                            if let Some(existing_items) = self.content.get_mut(&cur_section) {
277                                existing_items.extend(item_group.drain(..));
278                            }
279                        } else {
280                            self.last_error = IniReaderError::Duplicate;
281                            return Err(IniReaderError::Duplicate);
282                        }
283                    } else if !in_isolated_section || self.isolated_items_section != this_section {
284                        if !item_group.is_empty() {
285                            read_sections.push(cur_section.clone());
286                        }
287
288                        if !self.section_order.contains(&cur_section) {
289                            self.section_order.push(cur_section.clone());
290                        }
291
292                        self.content.insert(cur_section.clone(), item_group);
293                    }
294                }
295
296                in_isolated_section = false;
297                cur_section = this_section;
298                item_group = Vec::new();
299            }
300            // Handle normal lines within a section
301            else if !in_excluded_section && !cur_section.is_empty() {
302                let pos_equal = line.find('=');
303
304                // Handle lines without equals sign (or direct save sections)
305                if (self.store_any_line && pos_equal.is_none()) || in_direct_save_section {
306                    item_group.push(("{NONAME}".to_string(), line));
307                }
308                // Handle key=value pairs
309                else if let Some(pos) = pos_equal {
310                    let item_name = line[0..pos].trim().to_string();
311                    let item_value = if pos + 1 < line.len() {
312                        line[pos + 1..].trim().to_string()
313                    } else {
314                        String::new()
315                    };
316
317                    item_group.push((item_name, item_value));
318                }
319            } else if cur_section.is_empty() {
320                // Items outside of any section
321                self.last_error = IniReaderError::OutOfBound;
322                return Err(IniReaderError::OutOfBound);
323            }
324
325            // Check if all included sections have been read
326            if !self.include_sections.is_empty()
327                && read_sections
328                    .iter()
329                    .all(|s| self.include_sections.contains(s))
330            {
331                break;
332            }
333        }
334
335        // Save the final section
336        if !cur_section.is_empty() && (self.keep_empty_section || !item_group.is_empty()) {
337            if self.content.contains_key(&cur_section) {
338                if self.allow_dup_section_titles || in_isolated_section {
339                    // Merge with existing section
340                    if let Some(existing_items) = self.content.get_mut(&cur_section) {
341                        existing_items.extend(item_group.drain(..));
342                    }
343                } else if !self.content.get(&cur_section).unwrap().is_empty() {
344                    self.last_error = IniReaderError::Duplicate;
345                    return Err(IniReaderError::Duplicate);
346                }
347            } else if !in_isolated_section || self.isolated_items_section != cur_section {
348                if !item_group.is_empty() {
349                    read_sections.push(cur_section.clone());
350                }
351
352                if !self.section_order.contains(&cur_section) {
353                    self.section_order.push(cur_section.clone());
354                }
355
356                self.content.insert(cur_section, item_group);
357            }
358        }
359
360        self.parsed = true;
361        self.last_error = IniReaderError::None;
362        Ok(())
363    }
364
365    /// Parse an INI file
366    pub async fn parse_file(&mut self, path: &str) -> Result<(), IniReaderError> {
367        // Check if file exists
368        if !file_exists(path).await {
369            self.last_error = IniReaderError::NotExist;
370            return Err(IniReaderError::NotExist);
371        }
372
373        // Read the file
374        let content = file_get_async(path, None).await?;
375
376        // Parse the content
377        self.parse(&content)
378    }
379
380    /// Check if a section exists
381    pub fn section_exist(&self, section: &str) -> bool {
382        self.content.contains_key(section)
383    }
384
385    /// Get the count of sections
386    pub fn section_count(&self) -> usize {
387        self.content.len()
388    }
389
390    /// Get all section names
391    pub fn get_section_names(&self) -> &[String] {
392        &self.section_order
393    }
394
395    /// Set the current section
396    pub fn set_current_section(&mut self, section: &str) {
397        self.current_section = section.to_string();
398    }
399
400    /// Enter a section with the given name
401    pub fn enter_section(&mut self, section: &str) -> Result<(), IniReaderError> {
402        if !self.section_exist(section) {
403            self.last_error = IniReaderError::NotExist;
404            return Err(IniReaderError::NotExist);
405        }
406
407        self.current_section = section.to_string();
408        self.last_error = IniReaderError::None;
409        Ok(())
410    }
411
412    /// Check if an item exists in the given section
413    pub fn item_exist(&self, section: &str, item_name: &str) -> bool {
414        if !self.section_exist(section) {
415            return false;
416        }
417
418        self.content
419            .get(section)
420            .map(|items| items.iter().any(|(key, _)| key == item_name))
421            .unwrap_or(false)
422    }
423
424    /// Check if an item exists in the current section
425    pub fn item_exist_current(&self, item_name: &str) -> bool {
426        if self.current_section.is_empty() {
427            return false;
428        }
429
430        self.item_exist(&self.current_section, item_name)
431    }
432
433    /// Check if an item with given prefix exists in the section
434    pub fn item_prefix_exists(&self, section: &str, prefix: &str) -> bool {
435        if !self.section_exist(section) {
436            return false;
437        }
438
439        if let Some(items) = self.content.get(section) {
440            return items.iter().any(|(key, _)| key.starts_with(prefix));
441        }
442
443        false
444    }
445
446    /// Check if an item with given prefix exists in the current section
447    pub fn item_prefix_exist(&self, prefix: &str) -> bool {
448        if self.current_section.is_empty() {
449            return false;
450        }
451
452        self.item_prefix_exists(&self.current_section, prefix)
453    }
454
455    /// Get all items in a section
456    pub fn get_items(&self, section: &str) -> Result<Vec<(String, String)>, IniReaderError> {
457        if !self.parsed {
458            return Err(IniReaderError::NotParsed);
459        }
460
461        if !self.section_exist(section) {
462            return Err(IniReaderError::NotExist);
463        }
464
465        Ok(self.content.get(section).cloned().unwrap_or_default())
466    }
467
468    /// Get all items with the same name prefix in a section
469    pub fn get_all(&self, section: &str, item_name: &str) -> Result<Vec<String>, IniReaderError> {
470        if !self.parsed {
471            return Err(IniReaderError::NotParsed);
472        }
473
474        if !self.section_exist(section) {
475            return Err(IniReaderError::NotExist);
476        }
477
478        let mut results = Vec::new();
479
480        if let Some(items) = self.content.get(section) {
481            for (key, value) in items {
482                if key.starts_with(item_name) {
483                    results.push(value.clone());
484                }
485            }
486        }
487
488        Ok(results)
489    }
490
491    /// Get all items with the same name prefix in the current section
492    pub fn get_all_current(&self, item_name: &str) -> Result<Vec<String>, IniReaderError> {
493        if self.current_section.is_empty() {
494            return Err(IniReaderError::NotExist);
495        }
496
497        self.get_all(&self.current_section, item_name)
498    }
499
500    /// Get an item with the exact same name in the given section
501    pub fn get(&self, section: &str, item_name: &str) -> String {
502        if !self.parsed || !self.section_exist(section) {
503            return String::new();
504        }
505
506        self.content
507            .get(section)
508            .and_then(|items| items.iter().find(|(key, _)| key == item_name))
509            .map(|(_, value)| value.clone())
510            .unwrap_or_default()
511    }
512
513    /// Get an item with the exact same name in the current section
514    pub fn get_current(&self, item_name: &str) -> String {
515        if self.current_section.is_empty() {
516            return String::new();
517        }
518
519        self.get(&self.current_section, item_name)
520    }
521
522    /// Get a boolean value from the given section
523    pub fn get_bool(&self, section: &str, item_name: &str) -> bool {
524        self.get(section, item_name) == "true"
525    }
526
527    /// Get a boolean value from the current section
528    pub fn get_bool_current(&self, item_name: &str) -> bool {
529        self.get_current(item_name) == "true"
530    }
531
532    /// Get an integer value from the given section
533    pub fn get_int(&self, section: &str, item_name: &str) -> i32 {
534        self.get(section, item_name).parse::<i32>().unwrap_or(0)
535    }
536
537    /// Get an integer value from the current section
538    pub fn get_int_current(&self, item_name: &str) -> i32 {
539        self.get_current(item_name).parse::<i32>().unwrap_or(0)
540    }
541
542    /// Set a value in the given section
543    pub fn set(
544        &mut self,
545        section: &str,
546        item_name: &str,
547        item_val: &str,
548    ) -> Result<(), IniReaderError> {
549        if section.is_empty() {
550            self.last_error = IniReaderError::NotExist;
551            return Err(IniReaderError::NotExist);
552        }
553
554        if !self.parsed {
555            self.parsed = true;
556        }
557
558        // If section is {NONAME}, we're setting key directly to the current section
559        let real_section = if section == "{NONAME}" {
560            if self.current_section.is_empty() {
561                self.last_error = IniReaderError::NotExist;
562                return Err(IniReaderError::NotExist);
563            }
564            &self.current_section
565        } else {
566            section
567        };
568
569        // Add section if it doesn't exist
570        if !self.section_exist(real_section) {
571            self.section_order.push(real_section.to_string());
572            self.content.insert(real_section.to_string(), Vec::new());
573        }
574
575        // Update our content HashMap
576        if let Some(section_vec) = self.content.get_mut(real_section) {
577            section_vec.push((item_name.to_string(), item_val.to_string()));
578        }
579
580        self.last_error = IniReaderError::None;
581        Ok(())
582    }
583
584    /// Set a value in the current section
585    pub fn set_current(&mut self, item_name: &str, item_val: &str) -> Result<(), IniReaderError> {
586        if self.current_section.is_empty() {
587            self.last_error = IniReaderError::NotExist;
588            return Err(IniReaderError::NotExist);
589        }
590
591        // Handle the special case where item_name is {NONAME}
592        if item_name == "{NONAME}" {
593            return self.set_current_with_noname(item_val);
594        }
595
596        self.set(&self.current_section.clone(), item_name, item_val)
597    }
598
599    /// Set a boolean value in the given section
600    pub fn set_bool(
601        &mut self,
602        section: &str,
603        item_name: &str,
604        item_val: bool,
605    ) -> Result<(), IniReaderError> {
606        self.set(section, item_name, if item_val { "true" } else { "false" })
607    }
608
609    /// Set a boolean value in the current section
610    pub fn set_bool_current(
611        &mut self,
612        item_name: &str,
613        item_val: bool,
614    ) -> Result<(), IniReaderError> {
615        if self.current_section.is_empty() {
616            self.last_error = IniReaderError::NotExist;
617            return Err(IniReaderError::NotExist);
618        }
619
620        let value = if item_val { "true" } else { "false" };
621        self.set(&self.current_section.clone(), item_name, value)
622    }
623
624    /// Set an integer value in the given section
625    pub fn set_int(
626        &mut self,
627        section: &str,
628        item_name: &str,
629        item_val: i32,
630    ) -> Result<(), IniReaderError> {
631        self.set(section, item_name, &item_val.to_string())
632    }
633
634    /// Set an integer value in the current section
635    pub fn set_int_current(
636        &mut self,
637        item_name: &str,
638        item_val: i32,
639    ) -> Result<(), IniReaderError> {
640        if self.current_section.is_empty() {
641            self.last_error = IniReaderError::NotExist;
642            return Err(IniReaderError::NotExist);
643        }
644
645        self.set(
646            &self.current_section.clone(),
647            item_name,
648            &item_val.to_string(),
649        )
650    }
651
652    /// Export the INI to a string
653    pub fn to_string(&self) -> String {
654        if !self.parsed {
655            return String::new();
656        }
657
658        let mut result = String::new();
659
660        for section_name in &self.section_order {
661            // Add section header
662            result.push_str(&format!("[{}]\n", section_name));
663
664            if let Some(section) = self.content.get(section_name) {
665                if section.is_empty() {
666                    result.push('\n');
667                    continue;
668                }
669
670                // Add all items in this section
671                for (key, value) in section {
672                    let mut value = value.clone();
673                    Self::process_escape_char_reverse(&mut value);
674
675                    if key != "{NONAME}" {
676                        result.push_str(&format!("{}={}\n", key, value));
677                    } else {
678                        result.push_str(&format!("{}\n", value));
679                    }
680                }
681
682                // Add extra newline after section
683                result.push('\n');
684            }
685        }
686
687        result
688    }
689
690    /// Export the INI to a file
691    pub fn to_file<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
692        if !self.parsed {
693            return Err(io::Error::new(io::ErrorKind::InvalidData, "INI not parsed"));
694        }
695
696        let content = self.to_string();
697        std::fs::write(path, content)?;
698        Ok(())
699    }
700
701    /// Set a value in the current section with {NONAME} key
702    /// This is used in patterns like set_current("{NONAME}", "value")
703    pub fn set_current_with_noname(&mut self, item_val: &str) -> Result<(), IniReaderError> {
704        if self.current_section.is_empty() {
705            self.last_error = IniReaderError::NotExist;
706            return Err(IniReaderError::NotExist);
707        }
708
709        self.set(&self.current_section.clone(), "{NONAME}", item_val)
710    }
711}