Skip to main content

openmw_config/config/
encodingsetting.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2// Copyright (c) 2025 Dave Corley (S3kshun8)
3
4use std::fmt;
5
6use crate::{ConfigError, GameSetting, GameSettingMeta, bail_config};
7
8/// The text encoding used to interpret byte strings in plugin data.
9///
10/// `OpenMW` supports exactly three Windows code-page encodings; any other value in an
11/// `encoding=` line is rejected with [`ConfigError::BadEncoding`](crate::ConfigError::BadEncoding).
12#[derive(Debug, Copy, Clone, Eq, PartialEq)]
13#[non_exhaustive]
14pub enum EncodingType {
15    /// Windows code page 1250 — Central European (Polish, Czech, Slovak, …).
16    WIN1250,
17    /// Windows code page 1251 — Cyrillic (Russian, Ukrainian, …).
18    WIN1251,
19    /// Windows code page 1252 — Western European (English, French, German, …). The default.
20    WIN1252,
21}
22
23impl std::fmt::Display for EncodingType {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        let value = match self {
26            EncodingType::WIN1250 => "win1250",
27            EncodingType::WIN1251 => "win1251",
28            EncodingType::WIN1252 => "win1252",
29        };
30
31        writeln!(f, "{value}")
32    }
33}
34
35/// A parsed `encoding=` entry from an `openmw.cfg` file.
36///
37/// At most one `encoding=` entry is meaningful per resolved configuration chain (singleton
38/// semantics). The encoding affects how byte strings in plugin records are decoded.
39#[derive(Debug, Clone)]
40pub struct EncodingSetting {
41    meta: GameSettingMeta,
42    encoding: EncodingType,
43}
44
45impl EncodingSetting {
46    /// The parsed encoding type.
47    #[must_use]
48    pub fn value(&self) -> EncodingType {
49        self.encoding
50    }
51}
52
53impl PartialEq for EncodingSetting {
54    fn eq(&self, other: &Self) -> bool {
55        self.encoding == other.encoding
56    }
57}
58
59impl GameSetting for EncodingSetting {
60    fn meta(&self) -> &GameSettingMeta {
61        &self.meta
62    }
63}
64
65impl fmt::Display for EncodingSetting {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        write!(f, "{}encoding={}", self.meta.comment, self.encoding)
68    }
69}
70
71impl<P: AsRef<std::path::Path>> TryFrom<(String, P, &mut String)> for EncodingSetting {
72    type Error = ConfigError;
73
74    fn try_from(
75        (value, source_config, comment): (String, P, &mut String),
76    ) -> Result<Self, Self::Error> {
77        let source_config = source_config.as_ref().to_path_buf();
78
79        let encoding = match value.as_str() {
80            "win1250" => EncodingType::WIN1250,
81            "win1251" => EncodingType::WIN1251,
82            "win1252" => EncodingType::WIN1252,
83            _ => bail_config!(bad_encoding, value, source_config),
84        };
85
86        let meta = GameSettingMeta {
87            source_config,
88            comment: comment.to_owned(),
89        };
90        comment.clear();
91
92        Ok(EncodingSetting { meta, encoding })
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use std::path::PathBuf;
100
101    fn dummy_path() -> PathBuf {
102        PathBuf::from("/tmp/test.cfg")
103    }
104
105    fn dummy_comment() -> String {
106        "#OpenMW-Config Test Suite\n\n\n\n#EncodingSetting\n\n\n\n".into()
107    }
108
109    #[test]
110    fn test_valid_encodings() {
111        let encodings = vec![
112            ("win1250", EncodingType::WIN1250),
113            ("win1251", EncodingType::WIN1251),
114            ("win1252", EncodingType::WIN1252),
115        ];
116
117        for (input, expected_variant) in encodings {
118            let setting =
119                EncodingSetting::try_from((input.to_string(), dummy_path(), &mut dummy_comment()))
120                    .unwrap();
121
122            assert_eq!(setting.encoding, expected_variant);
123        }
124    }
125
126    #[test]
127    fn test_invalid_encoding() {
128        let err =
129            EncodingSetting::try_from(("utf8".to_string(), dummy_path(), &mut dummy_comment()));
130
131        assert!(matches!(err, Err(ConfigError::BadEncoding { .. })));
132    }
133
134    #[test]
135    fn test_empty_encoding_string() {
136        let err = EncodingSetting::try_from((String::new(), dummy_path(), &mut dummy_comment()));
137        assert!(matches!(err, Err(ConfigError::BadEncoding { .. })));
138    }
139
140    #[test]
141    fn test_source_path_preservation() {
142        let path = PathBuf::from("/some/path/to/config.cfg");
143        let setting = EncodingSetting::try_from((
144            "win1251".to_string(),
145            path.as_path(),
146            &mut dummy_comment(),
147        ))
148        .unwrap();
149
150        assert_eq!(setting.meta.source_config, path);
151    }
152
153    #[test]
154    fn test_display_trait_output() {
155        let setting = EncodingSetting::try_from((
156            "win1250".to_string(),
157            dummy_path().as_path(),
158            &mut dummy_comment(),
159        ))
160        .unwrap();
161
162        let rendered = setting.to_string();
163        assert_eq!(
164            rendered.trim(),
165            format!("{}encoding=win1250", dummy_comment())
166        );
167    }
168}