physis/
cfg.rs

1// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4use crate::{ByteBuffer, ByteSpan};
5use std::collections::HashMap;
6use std::io::{BufRead, BufReader, BufWriter, Cursor, Write};
7
8/// Represents a collection of keys, mapped to their values.
9#[derive(Debug)]
10pub struct ConfigMap {
11    /// A map of setting name to value.
12    pub keys: Vec<(String, String)>,
13}
14
15/// Represents a config file, which is made up of categories and settings. Categories may have zero to one setting.
16#[derive(Debug)]
17pub struct ConfigFile {
18    /// The categories present in this config file.
19    pub categories: Vec<String>,
20    /// A mapping of category to keys.
21    pub settings: HashMap<String, ConfigMap>,
22}
23
24impl ConfigFile {
25    /// Parses an existing config file.
26    pub fn from_existing(buffer: ByteSpan) -> Option<ConfigFile> {
27        let mut cfg = ConfigFile {
28            categories: Vec::new(),
29            settings: HashMap::new(),
30        };
31
32        let cursor = Cursor::new(buffer);
33        let reader = BufReader::new(cursor);
34
35        let mut current_category: Option<String> = None;
36
37        for line in reader.lines().map_while(Result::ok) {
38            if !line.is_empty() && line != "\0" {
39                if line.contains('<') || line.contains('>') {
40                    // Category
41                    let name = &line[1..line.len() - 1];
42                    current_category = Some(String::from(name));
43                    cfg.categories.push(String::from(name));
44                } else if let (Some(category), Some((key, value))) =
45                    (&current_category, line.split_once('\t'))
46                {
47                    // Key-value pair
48                    cfg.settings
49                        .entry(category.clone())
50                        .or_insert_with(|| ConfigMap { keys: Vec::new() });
51                    cfg.settings
52                        .get_mut(category)?
53                        .keys
54                        .push((key.to_string(), value.to_string()));
55                }
56            }
57        }
58
59        Some(cfg)
60    }
61
62    /// Writes an existing config file to a buffer.
63    pub fn write_to_buffer(&self) -> Option<ByteBuffer> {
64        let mut buffer = ByteBuffer::new();
65
66        {
67            let cursor = Cursor::new(&mut buffer);
68            let mut writer = BufWriter::new(cursor);
69
70            for category in &self.categories {
71                writer
72                    .write_all(format!("\r\n<{}>\r\n", category).as_ref())
73                    .ok()?;
74
75                if self.settings.contains_key(category) {
76                    for key in &self.settings[category].keys {
77                        writer
78                            .write_all(format!("{}\t{}\r\n", key.0, key.1).as_ref())
79                            .ok()?;
80                    }
81                }
82            }
83
84            writer.write_all(b"\0").ok()?;
85        }
86
87        Some(buffer)
88    }
89
90    /// Checks if the CFG contains a key named `select_key`
91    pub fn has_key(&self, select_key: &str) -> bool {
92        for map in self.settings.values() {
93            for (key, _) in &map.keys {
94                if select_key == key {
95                    return true;
96                }
97            }
98        }
99
100        false
101    }
102
103    /// Checks if the CFG contains a category named `select_category`
104    pub fn has_category(&self, select_category: &str) -> bool {
105        self.settings.contains_key(select_category)
106    }
107
108    /// Sets the value to `new_value` of `select_key`
109    pub fn set_value(&mut self, select_key: &str, new_value: &str) {
110        for keys in self.settings.values_mut() {
111            for (key, value) in &mut keys.keys {
112                if select_key == key {
113                    *value = new_value.to_string();
114                }
115            }
116        }
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use std::fs::read;
123    use std::path::PathBuf;
124
125    use super::*;
126
127    fn common_setup() -> ConfigFile {
128        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
129        d.push("resources/tests");
130        d.push("FFXIV.cfg");
131
132        ConfigFile::from_existing(&read(d).unwrap()).unwrap()
133    }
134
135    fn common_setup_modified() -> ByteBuffer {
136        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
137        d.push("resources/tests");
138        d.push("FFXIV.modified.cfg");
139
140        read(d).unwrap()
141    }
142
143    fn common_setup_invalid() -> ByteBuffer {
144        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
145        d.push("resources/tests");
146        d.push("random");
147
148        read(d).unwrap()
149    }
150
151    #[test]
152    fn basic_parsing() {
153        let cfg = common_setup();
154
155        assert!(cfg.has_key("TextureFilterQuality"));
156        assert!(cfg.has_category("Cutscene Settings"));
157    }
158
159    #[test]
160    fn basic_writing() {
161        let mut cfg = common_setup();
162        let modified_cfg = common_setup_modified();
163
164        cfg.set_value("CutsceneMovieOpening", "1");
165
166        let cfg_buffer = cfg.write_to_buffer().unwrap();
167
168        assert_eq!(modified_cfg, cfg_buffer);
169    }
170
171    #[test]
172    fn test_invalid() {
173        let cfg = common_setup_invalid();
174
175        // Feeding it invalid data should not panic
176        ConfigFile::from_existing(&cfg);
177    }
178}