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 NotWritable(PathBuf),
224 SubconfigNotLoaded(PathBuf),
227 MaxDepthExceeded(PathBuf),
229 PlatformPathUnavailable(&'static str),
231}
232
233fn line_suffix(line: Option<usize>) -> String {
234 line.map_or_else(String::new, |line| format!(" at line {line}"))
235}
236
237fn duplicate_message(file: &str, kind: &str, config_path: &Path, line: Option<usize>) -> String {
238 format!(
239 "{file} has appeared in the {kind} list twice. Its second occurence was in: {}{}",
240 config_path.display(),
241 line_suffix(line)
242 )
243}
244
245fn cannot_find_root_message(local: &Path, global: &Path) -> String {
246 format!(
247 "OpenMW root config discovery found no openmw.cfg at local path {} or global config path {}",
248 local.display(),
249 global.display()
250 )
251}
252
253impl fmt::Display for ConfigError {
254 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255 match self {
256 ConfigError::InvalidGameSetting {
257 value,
258 config_path,
259 line,
260 } => write!(
261 f,
262 "Invalid fallback setting '{}' in config file '{}'{}",
263 value,
264 config_path.display(),
265 line_suffix(*line)
266 ),
267 ConfigError::Io(e) => write!(f, "IO error: {e}"),
268 ConfigError::NotFileOrDirectory(config_path) => write!(
269 f,
270 "Unable to determine whether {} was a file or directory, refusing to read.",
271 config_path.display()
272 ),
273 ConfigError::CannotFind(config_path) => write!(
274 f,
275 "An openmw.cfg does not exist at: {}",
276 config_path.display()
277 ),
278 ConfigError::CannotFindRootConfig { local, global } => {
279 write!(f, "{}", cannot_find_root_message(local, global))
280 }
281 ConfigError::DuplicateContentFile {
282 file,
283 config_path,
284 line,
285 } => f.write_str(&duplicate_message(
286 file,
287 "content files",
288 config_path,
289 *line,
290 )),
291 ConfigError::CannotAddContentFile { file, config_path } => write!(
292 f,
293 "{file} cannot be added to the configuration map as a content file because it was already defined by: {}",
294 config_path.display()
295 ),
296 ConfigError::DuplicateGroundcoverFile {
297 file,
298 config_path,
299 line,
300 } => f.write_str(&duplicate_message(file, "groundcover", config_path, *line)),
301 ConfigError::CannotAddGroundcoverFile { file, config_path } => write!(
302 f,
303 "{file} cannot be added to the configuration map as a groundcover plugin because it was already defined by: {}",
304 config_path.display()
305 ),
306 ConfigError::DuplicateArchiveFile {
307 file,
308 config_path,
309 line,
310 } => f.write_str(&duplicate_message(file, "BSA/Archive", config_path, *line)),
311 ConfigError::CannotAddArchiveFile { file, config_path } => write!(
312 f,
313 "{file} cannot be added to the configuration map as a fallback-archive because it was already defined by: {}",
314 config_path.display()
315 ),
316 ConfigError::BadEncoding {
317 value,
318 config_path,
319 line,
320 } => write!(
321 f,
322 "Invalid encoding type: {value} in config file {}{}",
323 config_path.display(),
324 line_suffix(*line)
325 ),
326 ConfigError::InvalidLine {
327 value,
328 config_path,
329 line,
330 } => write!(
331 f,
332 "Invalid pair in openmw.cfg {value} was defined by {}{}",
333 config_path.display(),
334 line_suffix(*line)
335 ),
336 ConfigError::NotWritable(path) => {
337 write!(f, "Target path is not writable: {}", path.display())
338 }
339 ConfigError::SubconfigNotLoaded(path) => write!(
340 f,
341 "Cannot save to {}; it is not part of the loaded configuration chain",
342 path.display()
343 ),
344 ConfigError::MaxDepthExceeded(path) => write!(
345 f,
346 "Maximum config= nesting depth exceeded while loading {}",
347 path.display()
348 ),
349 ConfigError::PlatformPathUnavailable(kind) => {
350 write!(f, "Failed to resolve platform default {kind} path")
351 }
352 }
353 }
354}
355
356impl std::error::Error for ConfigError {}
357
358impl From<std::io::Error> for ConfigError {
359 fn from(err: std::io::Error) -> Self {
360 ConfigError::Io(err)
361 }
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367
368 #[test]
369 fn test_display_messages_include_key_context() {
370 let path = PathBuf::from("/tmp/openmw.cfg");
371
372 let cannot_find = ConfigError::CannotFind(path.clone()).to_string();
373 assert!(cannot_find.contains("openmw.cfg"));
374
375 let cannot_find_root = ConfigError::CannotFindRootConfig {
376 local: PathBuf::from("/tmp/local/openmw.cfg"),
377 global: PathBuf::from("/tmp/global/openmw.cfg"),
378 }
379 .to_string();
380 assert!(cannot_find_root.contains("/tmp/local/openmw.cfg"));
381 assert!(cannot_find_root.contains("/tmp/global/openmw.cfg"));
382
383 let duplicate = ConfigError::DuplicateContentFile {
384 file: "Morrowind.esm".into(),
385 config_path: path.clone(),
386 line: None,
387 }
388 .to_string();
389 assert!(duplicate.contains("Morrowind.esm"));
390
391 let invalid_line = ConfigError::InvalidLine {
392 value: "broken".into(),
393 config_path: path,
394 line: Some(42),
395 }
396 .to_string();
397 assert!(invalid_line.contains("broken"));
398 assert!(invalid_line.contains("line 42"));
399 }
400
401 #[test]
402 fn test_from_io_error_wraps_variant() {
403 let io = std::io::Error::other("boom");
404 let converted: ConfigError = io.into();
405 assert!(matches!(converted, ConfigError::Io(_)));
406 }
407}