1use std::{
5 fmt,
6 path::{Path, PathBuf},
7};
8
9macro_rules! config_err {
10 (invalid_game_setting, $value:expr, $path:expr) => {
12 $crate::ConfigError::InvalidGameSetting {
13 value: $value.to_string(),
14 config_path: $path.to_path_buf(),
15 line: None,
16 }
17 };
18
19 (invalid_game_setting, $value:expr, $path:expr, $line:expr) => {
20 $crate::ConfigError::InvalidGameSetting {
21 value: $value.to_string(),
22 config_path: $path.to_path_buf(),
23 line: Some($line),
24 }
25 };
26
27 (not_file_or_directory, $config_path:expr) => {
28 $crate::ConfigError::NotFileOrDirectory($config_path.to_path_buf())
29 };
30
31 (cannot_find, $config_path:expr) => {
32 $crate::ConfigError::CannotFind($config_path.to_path_buf())
33 };
34
35 (duplicate_content_file, $content_file:expr, $config_path:expr) => {
36 $crate::ConfigError::DuplicateContentFile {
37 file: $content_file,
38 config_path: $config_path.to_path_buf(),
39 line: None,
40 }
41 };
42
43 (duplicate_content_file, $content_file:expr, $config_path:expr, $line:expr) => {
44 $crate::ConfigError::DuplicateContentFile {
45 file: $content_file,
46 config_path: $config_path.to_path_buf(),
47 line: Some($line),
48 }
49 };
50
51 (duplicate_archive_file, $archive_file:expr, $config_path:expr) => {
52 $crate::ConfigError::DuplicateArchiveFile {
53 file: $archive_file,
54 config_path: $config_path.to_path_buf(),
55 line: None,
56 }
57 };
58
59 (duplicate_archive_file, $archive_file:expr, $config_path:expr, $line:expr) => {
60 $crate::ConfigError::DuplicateArchiveFile {
61 file: $archive_file,
62 config_path: $config_path.to_path_buf(),
63 line: Some($line),
64 }
65 };
66
67 (archive_already_defined, $content_file:expr, $config_path:expr) => {
68 $crate::ConfigError::CannotAddArchiveFile {
69 file: $content_file,
70 config_path: $config_path.to_path_buf(),
71 }
72 };
73
74 (content_already_defined, $content_file:expr, $config_path:expr) => {
75 $crate::ConfigError::CannotAddContentFile {
76 file: $content_file,
77 config_path: $config_path.to_path_buf(),
78 }
79 };
80
81 (groundcover_already_defined, $groundcover_file:expr, $config_path:expr) => {
82 $crate::ConfigError::CannotAddGroundcoverFile {
83 file: $groundcover_file,
84 config_path: $config_path.to_path_buf(),
85 }
86 };
87
88 (duplicate_groundcover_file, $groundcover_file:expr, $config_path:expr) => {
89 $crate::ConfigError::DuplicateGroundcoverFile {
90 file: $groundcover_file,
91 config_path: $config_path.to_path_buf(),
92 line: None,
93 }
94 };
95
96 (duplicate_groundcover_file, $groundcover_file:expr, $config_path:expr, $line:expr) => {
97 $crate::ConfigError::DuplicateGroundcoverFile {
98 file: $groundcover_file,
99 config_path: $config_path.to_path_buf(),
100 line: Some($line),
101 }
102 };
103
104 (bad_encoding, $encoding:expr, $config_path:expr) => {
105 $crate::ConfigError::BadEncoding {
106 value: $encoding,
107 config_path: $config_path,
108 line: None,
109 }
110 };
111
112 (bad_encoding, $encoding:expr, $config_path:expr, $line:expr) => {
113 $crate::ConfigError::BadEncoding {
114 value: $encoding,
115 config_path: $config_path,
116 line: Some($line),
117 }
118 };
119
120 (invalid_line, $value:expr, $config_path:expr) => {
121 $crate::ConfigError::InvalidLine {
122 value: $value,
123 config_path: $config_path,
124 line: None,
125 }
126 };
127
128 (invalid_line, $value:expr, $config_path:expr, $line:expr) => {
129 $crate::ConfigError::InvalidLine {
130 value: $value,
131 config_path: $config_path,
132 line: Some($line),
133 }
134 };
135
136 (not_writable, $path:expr) => {
137 $crate::ConfigError::NotWritable($path.to_path_buf())
138 };
139
140 (subconfig_not_loaded, $path:expr) => {
141 $crate::ConfigError::SubconfigNotLoaded($path.to_path_buf())
142 };
143
144 (max_depth_exceeded, $path:expr) => {
145 $crate::ConfigError::MaxDepthExceeded($path.to_path_buf())
146 };
147
148 (io, $err:expr) => {
150 $crate::ConfigError::Io($err)
151 };
152}
153
154pub(crate) use config_err;
155
156macro_rules! bail_config {
157 ($($tt:tt)*) => {{
158 return Err($crate::config::error::config_err!($($tt)*));
159 }};
160}
161pub(crate) use bail_config;
162
163#[derive(Debug)]
165#[non_exhaustive]
166pub enum ConfigError {
167 DuplicateContentFile {
169 file: String,
170 config_path: PathBuf,
171 line: Option<usize>,
172 },
173 DuplicateArchiveFile {
175 file: String,
176 config_path: PathBuf,
177 line: Option<usize>,
178 },
179 CannotAddContentFile { file: String, config_path: PathBuf },
182 CannotAddArchiveFile { file: String, config_path: PathBuf },
185 DuplicateGroundcoverFile {
187 file: String,
188 config_path: PathBuf,
189 line: Option<usize>,
190 },
191 CannotAddGroundcoverFile { file: String, config_path: PathBuf },
194 InvalidGameSetting {
196 value: String,
197 config_path: PathBuf,
198 line: Option<usize>,
199 },
200 BadEncoding {
203 value: String,
204 config_path: PathBuf,
205 line: Option<usize>,
206 },
207 InvalidLine {
209 value: String,
210 config_path: PathBuf,
211 line: Option<usize>,
212 },
213 Io(std::io::Error),
215 NotFileOrDirectory(PathBuf),
217 CannotFind(PathBuf),
219 CannotFindRootConfig { local: PathBuf, global: PathBuf },
222 CannotFindAnyConfig {
225 local: PathBuf,
226 global: PathBuf,
227 user: PathBuf,
228 },
229 NotWritable(PathBuf),
231 SubconfigNotLoaded(PathBuf),
234 MaxDepthExceeded(PathBuf),
237 PlatformPathUnavailable(&'static str),
242}
243
244fn line_suffix(line: Option<usize>) -> String {
245 line.map_or_else(String::new, |line| format!(" at line {line}"))
246}
247
248fn duplicate_message(file: &str, kind: &str, config_path: &Path, line: Option<usize>) -> String {
249 format!(
250 "{file} has appeared in the {kind} list twice. Its second occurence was in: {}{}",
251 config_path.display(),
252 line_suffix(line)
253 )
254}
255
256fn global_candidate(global: &Path) -> String {
257 if global.as_os_str().is_empty() {
258 "no global config path on this platform".to_string()
259 } else {
260 format!("global config path {}", global.display())
261 }
262}
263
264fn cannot_find_root_message(local: &Path, global: &Path) -> String {
265 format!(
266 "OpenMW root config discovery found no openmw.cfg at local path {}, and {}",
267 local.display(),
268 global_candidate(global)
269 )
270}
271
272fn cannot_find_any_message(local: &Path, global: &Path, user: &Path) -> String {
273 format!(
274 "OpenMW config discovery found no openmw.cfg at local path {}, {}, or user config path {}",
275 local.display(),
276 global_candidate(global),
277 user.display()
278 )
279}
280
281fn fmt_duplicate_or_add_error(
282 error: &ConfigError,
283 f: &mut fmt::Formatter<'_>,
284) -> Option<fmt::Result> {
285 match error {
286 ConfigError::DuplicateContentFile {
287 file,
288 config_path,
289 line,
290 } => Some(f.write_str(&duplicate_message(
291 file,
292 "content files",
293 config_path,
294 *line,
295 ))),
296 ConfigError::CannotAddContentFile { file, config_path } => Some(write!(
297 f,
298 "{file} cannot be added to the configuration map as a content file because it was already defined by: {}",
299 config_path.display()
300 )),
301 ConfigError::DuplicateGroundcoverFile {
302 file,
303 config_path,
304 line,
305 } => Some(f.write_str(&duplicate_message(file, "groundcover", config_path, *line))),
306 ConfigError::CannotAddGroundcoverFile { file, config_path } => Some(write!(
307 f,
308 "{file} cannot be added to the configuration map as a groundcover plugin because it was already defined by: {}",
309 config_path.display()
310 )),
311 ConfigError::DuplicateArchiveFile {
312 file,
313 config_path,
314 line,
315 } => Some(f.write_str(&duplicate_message(file, "BSA/Archive", config_path, *line))),
316 ConfigError::CannotAddArchiveFile { file, config_path } => Some(write!(
317 f,
318 "{file} cannot be added to the configuration map as a fallback-archive because it was already defined by: {}",
319 config_path.display()
320 )),
321 _ => None,
322 }
323}
324
325impl fmt::Display for ConfigError {
326 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
327 if let Some(result) = fmt_duplicate_or_add_error(self, f) {
328 return result;
329 }
330
331 match self {
332 ConfigError::InvalidGameSetting {
333 value,
334 config_path,
335 line,
336 } => write!(
337 f,
338 "Invalid fallback setting '{}' in config file '{}'{}",
339 value,
340 config_path.display(),
341 line_suffix(*line)
342 ),
343 ConfigError::Io(e) => write!(f, "IO error: {e}"),
344 ConfigError::NotFileOrDirectory(config_path) => write!(
345 f,
346 "Unable to determine whether {} was a file or directory, refusing to read.",
347 config_path.display()
348 ),
349 ConfigError::CannotFind(config_path) => write!(
350 f,
351 "An openmw.cfg does not exist at: {}",
352 config_path.display()
353 ),
354 ConfigError::CannotFindRootConfig { local, global } => {
355 write!(f, "{}", cannot_find_root_message(local, global))
356 }
357 ConfigError::CannotFindAnyConfig {
358 local,
359 global,
360 user,
361 } => write!(f, "{}", cannot_find_any_message(local, global, user)),
362 ConfigError::DuplicateContentFile { .. }
363 | ConfigError::CannotAddContentFile { .. }
364 | ConfigError::DuplicateGroundcoverFile { .. }
365 | ConfigError::CannotAddGroundcoverFile { .. }
366 | ConfigError::DuplicateArchiveFile { .. }
367 | ConfigError::CannotAddArchiveFile { .. } => {
368 unreachable!("duplicate/add errors are handled before the main display match")
369 }
370 ConfigError::BadEncoding {
371 value,
372 config_path,
373 line,
374 } => write!(
375 f,
376 "Invalid encoding type: {value} in config file {}{}",
377 config_path.display(),
378 line_suffix(*line)
379 ),
380 ConfigError::InvalidLine {
381 value,
382 config_path,
383 line,
384 } => write!(
385 f,
386 "Invalid pair in openmw.cfg {value} was defined by {}{}",
387 config_path.display(),
388 line_suffix(*line)
389 ),
390 ConfigError::NotWritable(path) => {
391 write!(f, "Target path is not writable: {}", path.display())
392 }
393 ConfigError::SubconfigNotLoaded(path) => write!(
394 f,
395 "Cannot save to {}; it is not part of the loaded configuration chain",
396 path.display()
397 ),
398 ConfigError::MaxDepthExceeded(path) => write!(
399 f,
400 "Maximum config= nesting depth exceeded while loading {}",
401 path.display()
402 ),
403 ConfigError::PlatformPathUnavailable(kind) => {
404 write!(f, "Failed to resolve platform default {kind} path")
405 }
406 }
407 }
408}
409
410impl std::error::Error for ConfigError {
411 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
412 match self {
413 ConfigError::Io(error) => Some(error),
414 _ => None,
415 }
416 }
417}
418
419impl From<std::io::Error> for ConfigError {
420 fn from(err: std::io::Error) -> Self {
421 ConfigError::Io(err)
422 }
423}
424
425#[cfg(test)]
426mod tests {
427 use super::*;
428
429 #[test]
430 fn test_display_messages_include_key_context() {
431 let path = PathBuf::from("/tmp/openmw.cfg");
432
433 let cannot_find = ConfigError::CannotFind(path.clone()).to_string();
434 assert!(cannot_find.contains("openmw.cfg"));
435
436 let cannot_find_root = ConfigError::CannotFindRootConfig {
437 local: PathBuf::from("/tmp/local/openmw.cfg"),
438 global: PathBuf::from("/tmp/global/openmw.cfg"),
439 }
440 .to_string();
441 assert!(cannot_find_root.contains("/tmp/local/openmw.cfg"));
442 assert!(cannot_find_root.contains("/tmp/global/openmw.cfg"));
443
444 let without_global = ConfigError::CannotFindRootConfig {
445 local: PathBuf::from("/tmp/local/openmw.cfg"),
446 global: PathBuf::new(),
447 }
448 .to_string();
449 assert!(without_global.contains("/tmp/local/openmw.cfg"));
450 assert!(without_global.contains("no global config path on this platform"));
451
452 let cannot_find_any = ConfigError::CannotFindAnyConfig {
453 local: PathBuf::from("/tmp/local/openmw.cfg"),
454 global: PathBuf::from("/tmp/global/openmw.cfg"),
455 user: PathBuf::from("/tmp/user/openmw.cfg"),
456 }
457 .to_string();
458 assert!(cannot_find_any.contains("/tmp/local/openmw.cfg"));
459 assert!(cannot_find_any.contains("/tmp/global/openmw.cfg"));
460 assert!(cannot_find_any.contains("/tmp/user/openmw.cfg"));
461
462 let duplicate = ConfigError::DuplicateContentFile {
463 file: "Morrowind.esm".into(),
464 config_path: path.clone(),
465 line: None,
466 }
467 .to_string();
468 assert!(duplicate.contains("Morrowind.esm"));
469
470 let invalid_line = ConfigError::InvalidLine {
471 value: "broken".into(),
472 config_path: path,
473 line: Some(42),
474 }
475 .to_string();
476 assert!(invalid_line.contains("broken"));
477 assert!(invalid_line.contains("line 42"));
478 }
479
480 #[test]
481 fn test_io_error_is_the_source() {
482 use std::error::Error;
483
484 let error = ConfigError::Io(std::io::Error::other("boom"));
485 assert_eq!(
486 error.source().map(ToString::to_string),
487 Some("boom".to_string())
488 );
489 assert!(
490 ConfigError::CannotFind(PathBuf::from("openmw.cfg"))
491 .source()
492 .is_none()
493 );
494 }
495
496 #[test]
497 fn test_from_io_error_wraps_variant() {
498 let io = std::io::Error::other("boom");
499 let converted: ConfigError = io.into();
500 assert!(matches!(converted, ConfigError::Io(_)));
501 }
502}