1use crate::config::strings;
5use std::{fmt, path::PathBuf};
6
7#[derive(Debug, Clone)]
14pub struct DirectorySetting {
15 pub meta: crate::GameSettingMeta,
16 original: String,
17 parsed: PathBuf,
18}
19
20impl std::fmt::Display for DirectorySetting {
21 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22 writeln!(f, "{}", self.original)
23 }
24}
25
26impl crate::GameSetting for DirectorySetting {
27 fn meta(&self) -> &crate::GameSettingMeta {
28 &self.meta
29 }
30
31 fn meta_mut(&mut self) -> &mut crate::GameSettingMeta {
32 &mut self.meta
33 }
34}
35
36impl DirectorySetting {
37 pub fn new<S: Into<String>>(value: S, source_config: PathBuf, comment: &mut String) -> Self {
42 let original = value.into();
43 let parse_base = if source_config.file_name().is_some_and(|f| f == "openmw.cfg") {
44 source_config.parent().unwrap_or(source_config.as_path())
45 } else {
46 source_config.as_path()
47 };
48 let parsed = strings::parse_data_directory(&parse_base, &original);
49
50 let meta = crate::GameSettingMeta {
51 source_config,
52 comment: std::mem::take(comment),
53 };
54
55 Self {
56 meta,
57 original,
58 parsed,
59 }
60 }
61
62 #[must_use]
66 pub fn original(&self) -> &String {
67 &self.original
68 }
69
70 #[must_use]
72 pub fn original_str(&self) -> &str {
73 &self.original
74 }
75
76 #[must_use]
81 pub fn parsed(&self) -> &std::path::Path {
82 &self.parsed
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89 use std::path::PathBuf;
90
91 #[test]
92 fn test_directory_setting_basic_construction() {
93 let config_path = PathBuf::from("/my/config");
94 let mut comment = "some comment".to_string();
95
96 let setting = DirectorySetting::new("data", config_path.clone(), &mut comment);
97
98 assert_eq!(setting.original, "data");
99 assert_eq!(setting.parsed, config_path.join("data"));
100 assert_eq!(setting.meta.source_config, config_path);
101 assert_eq!(setting.meta.comment, "some comment");
102 assert!(comment.is_empty()); }
104
105 #[test]
106 fn test_directory_setting_with_user_data_token() {
107 let config_path = PathBuf::from("/irrelevant");
108 let mut comment = String::new();
109
110 let setting = DirectorySetting::new("?userdata?/foo", config_path, &mut comment);
111
112 let expected_prefix = crate::default_userdata_path();
113 assert!(setting.parsed.starts_with(expected_prefix));
114 assert!(setting.parsed.ends_with("foo/"));
115 }
116
117 #[test]
118 fn test_directory_setting_with_user_config_token() {
119 let config_path = PathBuf::from("/config/dir");
120 let mut comment = String::new();
121
122 let setting = DirectorySetting::new("?userconfig?/bar", config_path, &mut comment);
123 dbg!(setting.parsed());
124
125 let expected_prefix = crate::default_config_path();
126 assert!(setting.parsed.starts_with(expected_prefix));
127 assert!(setting.parsed.ends_with("bar"));
128 }
129
130 #[test]
131 fn test_directory_setting_quoted_path() {
132 let config_path = PathBuf::from("/my/config");
133 let mut comment = String::new();
134
135 let setting =
136 DirectorySetting::new("\"path/with spaces\"", config_path.clone(), &mut comment);
137
138 assert_eq!(setting.original, "\"path/with spaces\"");
139 assert_eq!(setting.parsed, config_path.join("path").join("with spaces"));
140 }
141
142 #[test]
143 fn test_directory_setting_relative_path_normalization() {
144 let config_path = PathBuf::from("/my/config");
145 let mut comment = String::new();
146
147 let setting = DirectorySetting::new("subdir\\nested", config_path.clone(), &mut comment);
148
149 let expected = config_path.join("subdir").join("nested");
150 assert_eq!(setting.parsed, expected);
151 }
152
153 fn mock_path(path: &str) -> PathBuf {
154 PathBuf::from(path)
155 }
156
157 #[test]
158 fn test_dot_component_is_removed() {
159 let config = mock_path("/etc/openmw");
160 let mut comment = String::from("comment");
161 let setting = DirectorySetting::new("./data", config.clone(), &mut comment);
162 assert_eq!(setting.parsed(), &config.join("data"));
163 }
164
165 #[test]
166 fn test_double_dot_not_normalized() {
167 let config = mock_path("/home/user/.config/openmw");
169 let mut comment = String::from("comment");
170 let setting = DirectorySetting::new("../common", config.clone(), &mut comment);
171 let expected = config.join("../common");
172 assert_eq!(setting.parsed(), &expected);
173 }
174
175 #[test]
176 fn test_dot_components_not_normalized() {
177 let config = mock_path("/opt/game/config");
179 let mut comment = String::new();
180 let setting = DirectorySetting::new("foo/./bar/../baz", config.clone(), &mut comment);
181 let expected = config.join("foo/./bar/../baz");
182 assert_eq!(setting.parsed(), &expected);
183 }
184
185 #[test]
188 fn test_absolute_path_not_joined_to_config() {
189 let config = mock_path("/etc/openmw");
191 let mut comment = String::new();
192 let setting = DirectorySetting::new("/absolute/path/to/data", config, &mut comment);
193 assert_eq!(setting.parsed(), &PathBuf::from("/absolute/path/to/data"));
194 }
195
196 #[test]
197 fn test_absolute_path_original_preserved() {
198 let config = mock_path("/etc/openmw");
199 let mut comment = String::new();
200 let setting = DirectorySetting::new("/absolute/data", config, &mut comment);
201 assert_eq!(setting.original(), "/absolute/data");
202 assert_eq!(setting.original_str(), "/absolute/data");
203 }
204
205 #[test]
208 fn test_backslash_normalised_to_separator() {
209 let config = mock_path("/my/config");
211 let mut comment = String::new();
212 let setting = DirectorySetting::new("subdir\\nested\\leaf", config.clone(), &mut comment);
213 let expected = config.join("subdir").join("nested").join("leaf");
214 assert_eq!(setting.parsed(), &expected);
215 }
216
217 #[test]
218 fn test_mixed_separators_normalised() {
219 let config = mock_path("/my/config");
220 let mut comment = String::new();
221 let setting = DirectorySetting::new("a\\b/c", config.clone(), &mut comment);
222 let expected = config.join("a").join("b").join("c");
223 assert_eq!(setting.parsed(), &expected);
224 }
225
226 #[test]
229 fn test_quoted_path_stripped_of_quotes() {
230 let config = mock_path("/cfg");
231 let mut comment = String::new();
232 let setting = DirectorySetting::new("\"simple\"", config.clone(), &mut comment);
233 assert_eq!(setting.parsed(), &config.join("simple"));
234 }
235
236 #[test]
237 fn test_quoted_path_ampersand_escapes_next_char() {
238 let config = mock_path("/cfg");
240 let mut comment = String::new();
241 let setting = DirectorySetting::new("\"foo&\"bar\"", config.clone(), &mut comment);
243 assert_eq!(setting.parsed(), &config.join("foo\"bar"));
244 }
245
246 #[test]
247 fn test_quoted_path_ampersand_escapes_ampersand() {
248 let config = mock_path("/cfg");
249 let mut comment = String::new();
250 let setting = DirectorySetting::new("\"foo&&bar\"", config.clone(), &mut comment);
251 assert_eq!(setting.parsed(), &config.join("foo&bar"));
252 }
253
254 #[test]
255 fn test_original_preserves_quotes() {
256 let config = mock_path("/cfg");
258 let mut comment = String::new();
259 let setting = DirectorySetting::new("\"path with spaces\"", config, &mut comment);
260 assert_eq!(setting.original(), "\"path with spaces\"");
261 }
262
263 #[test]
266 fn test_userdata_token_only() {
267 let config = mock_path("/irrelevant");
268 let mut comment = String::new();
269 let setting = DirectorySetting::new("?userdata?", config, &mut comment);
270 assert_eq!(setting.parsed(), &crate::default_userdata_path());
272 }
273
274 #[test]
275 fn test_userconfig_token_only() {
276 let config = mock_path("/irrelevant");
277 let mut comment = String::new();
278 let setting = DirectorySetting::new("?userconfig?", config, &mut comment);
279 assert_eq!(setting.parsed(), &crate::default_config_path());
280 }
281
282 #[test]
283 fn test_userdata_token_with_nested_path() {
284 let config = mock_path("/irrelevant");
285 let mut comment = String::new();
286 let setting = DirectorySetting::new("?userdata?/saves/slot1", config, &mut comment);
287 let expected = crate::default_userdata_path().join("saves").join("slot1");
288 assert_eq!(setting.parsed(), &expected);
289 }
290
291 #[test]
292 fn test_local_token_only() {
293 let config = mock_path("/irrelevant");
294 let mut comment = String::new();
295 let setting = DirectorySetting::new("?local?", config, &mut comment);
296 assert_eq!(setting.parsed(), &crate::default_local_path());
297 }
298
299 #[test]
300 fn test_local_token_with_nested_path() {
301 let config = mock_path("/irrelevant");
302 let mut comment = String::new();
303 let setting = DirectorySetting::new("?local?/mods/common", config, &mut comment);
304 let expected = crate::default_local_path().join("mods").join("common");
305 assert_eq!(setting.parsed(), &expected);
306 }
307
308 #[test]
309 #[cfg(not(windows))]
310 fn test_global_token_only_on_supported_platforms() {
311 let config = mock_path("/irrelevant");
312 let mut comment = String::new();
313 let setting = DirectorySetting::new("?global?", config, &mut comment);
314 assert_eq!(setting.parsed(), &crate::default_global_path());
315 }
316
317 #[test]
318 #[cfg(not(windows))]
319 fn test_global_token_with_nested_path_on_supported_platforms() {
320 let config = mock_path("/irrelevant");
321 let mut comment = String::new();
322 let setting = DirectorySetting::new("?global?/openmw", config, &mut comment);
323 let expected = crate::default_global_path().join("openmw");
324 assert_eq!(setting.parsed(), &expected);
325 }
326
327 #[test]
328 #[cfg(windows)]
329 fn test_global_token_is_left_unexpanded_on_windows() {
330 let config = mock_path(r"C:\OpenMW");
331 let mut comment = String::new();
332 let setting = DirectorySetting::new("?global?/data", config.clone(), &mut comment);
333 let expected = config.join("?global?").join("data");
334 assert_eq!(setting.parsed(), &expected);
335 }
336
337 #[test]
340 fn test_source_config_stored_verbatim() {
341 let config = mock_path("/etc/openmw/openmw.cfg");
342 let mut comment = String::new();
343 let setting = DirectorySetting::new("data", config.clone(), &mut comment);
344 assert_eq!(setting.meta.source_config, config);
345 }
346
347 #[test]
348 fn test_comment_cleared_after_new() {
349 let config = mock_path("/etc/openmw");
350 let mut comment = String::from("# a comment\n");
351 let setting = DirectorySetting::new("data", config, &mut comment);
352 assert_eq!(setting.meta.comment, "# a comment\n");
353 assert!(
354 comment.is_empty(),
355 "comment should be cleared after construction"
356 );
357 }
358
359 #[test]
360 fn test_empty_comment_stays_empty() {
361 let config = mock_path("/etc/openmw");
362 let mut comment = String::new();
363 let setting = DirectorySetting::new("data", config, &mut comment);
364 assert!(setting.meta.comment.is_empty());
365 }
366}