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