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