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