Skip to main content

openmw_config/config/
encodingsetting.rs

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