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