Skip to main content

darklua_core/frontend/
error.rs

1use std::{
2    borrow::Cow,
3    cmp::Ordering,
4    collections::HashSet,
5    ffi::OsString,
6    fmt::{self, Display},
7    path::PathBuf,
8    str::Utf8Error,
9};
10
11use crate::{process::LuaSerializerError, rules::Rule, ParserError};
12
13use super::{
14    resources::ResourceError,
15    work_item::{WorkData, WorkItem, WorkStatus},
16};
17
18#[derive(Debug, Clone)]
19enum ErrorKind {
20    Parser {
21        path: PathBuf,
22        error: ParserError,
23    },
24    ResourceNotFound {
25        path: PathBuf,
26    },
27    ExpectedUtf8 {
28        path: PathBuf,
29        utf8_error: Utf8Error,
30    },
31    InvalidConfiguration {
32        path: PathBuf,
33    },
34    MultipleConfigurationFound {
35        paths: Vec<PathBuf>,
36    },
37    IO {
38        path: PathBuf,
39        error: String,
40    },
41    UncachedWork {
42        path: PathBuf,
43    },
44    RuleError {
45        path: PathBuf,
46        rule_name: String,
47        rule_number: Option<usize>,
48        error: String,
49    },
50    CyclicWork {
51        work: Vec<(WorkData, Vec<PathBuf>)>,
52    },
53    Deserialization {
54        message: String,
55        data_type: &'static str,
56    },
57    Serialization {
58        message: String,
59        data_type: &'static str,
60    },
61    InvalidResourcePath {
62        location: String,
63        message: String,
64    },
65    RequireUnknownResource {
66        path: PathBuf,
67    },
68    RequireCopiedResource {
69        path: PathBuf,
70    },
71    OsStringConversion {
72        os_string: OsString,
73    },
74    InvalidGlobPattern {
75        pattern: String,
76        message: String,
77    },
78    Custom {
79        message: Cow<'static, str>,
80    },
81}
82
83/// A type alias for `Result<T, DarkluaError>`.
84pub type DarkluaResult<T> = Result<T, DarkluaError>;
85
86/// The main error type for the darklua library.
87///
88/// This error type represents all possible errors that can occur during
89/// the processing of Lua files. It includes errors from parsing, file I/O,
90/// configuration, and rule application.
91#[derive(Debug, Clone)]
92pub struct DarkluaError {
93    kind: Box<ErrorKind>,
94    context: Option<Cow<'static, str>>,
95}
96
97impl DarkluaError {
98    fn new(kind: ErrorKind) -> Self {
99        Self {
100            kind: kind.into(),
101            context: None,
102        }
103    }
104
105    pub(crate) fn context(mut self, context: impl Into<Cow<'static, str>>) -> Self {
106        self.context = Some(context.into());
107        self
108    }
109
110    pub(crate) fn parser_error(path: impl Into<PathBuf>, error: ParserError) -> Self {
111        Self::new(ErrorKind::Parser {
112            path: path.into(),
113            error,
114        })
115    }
116
117    pub(crate) fn multiple_configuration_found(
118        configuration_files: impl Iterator<Item = PathBuf>,
119    ) -> Self {
120        Self::new(ErrorKind::MultipleConfigurationFound {
121            paths: configuration_files.collect(),
122        })
123    }
124
125    pub(crate) fn io_error(path: impl Into<PathBuf>, error: impl Into<String>) -> Self {
126        Self::new(ErrorKind::IO {
127            path: path.into(),
128            error: error.into(),
129        })
130    }
131
132    pub(crate) fn resource_not_found(path: impl Into<PathBuf>) -> Self {
133        Self::new(ErrorKind::ResourceNotFound { path: path.into() })
134    }
135
136    pub(crate) fn expected_utf8(path: impl Into<PathBuf>, utf8_error: Utf8Error) -> Self {
137        Self::new(ErrorKind::ExpectedUtf8 {
138            path: path.into(),
139            utf8_error,
140        })
141    }
142
143    pub(crate) fn invalid_configuration_file(path: impl Into<PathBuf>) -> Self {
144        Self::new(ErrorKind::InvalidConfiguration { path: path.into() })
145    }
146
147    pub(crate) fn uncached_work(path: impl Into<PathBuf>) -> Self {
148        Self::new(ErrorKind::UncachedWork { path: path.into() })
149    }
150
151    pub(crate) fn rule_error(
152        path: impl Into<PathBuf>,
153        rule: &dyn Rule,
154        rule_index: usize,
155        rule_error: impl Into<String>,
156    ) -> Self {
157        Self::new(ErrorKind::RuleError {
158            path: path.into(),
159            rule_name: rule.get_name().to_owned(),
160            rule_number: Some(rule_index),
161            error: rule_error.into(),
162        })
163    }
164
165    pub(crate) fn orphan_rule_error(
166        path: impl Into<PathBuf>,
167        rule: &dyn Rule,
168        rule_error: impl Into<String>,
169    ) -> Self {
170        Self::new(ErrorKind::RuleError {
171            path: path.into(),
172            rule_name: rule.get_name().to_owned(),
173            rule_number: None,
174            error: rule_error.into(),
175        })
176    }
177
178    pub(crate) fn cyclic_work(work_left: Vec<&WorkItem>) -> Self {
179        let source_left: HashSet<PathBuf> = work_left
180            .iter()
181            .map(|work| work.source().to_path_buf())
182            .collect();
183
184        let mut required_work: Vec<_> = work_left
185            .into_iter()
186            .filter(|work| work.total_required_content() != 0)
187            .filter_map(|work| match &work.status {
188                WorkStatus::NotStarted => None,
189                WorkStatus::InProgress(progress) => {
190                    let mut content: Vec<_> = progress
191                        .required_content()
192                        .filter(|path| source_left.contains(*path))
193                        .map(PathBuf::from)
194                        .collect();
195                    if content.is_empty() {
196                        None
197                    } else {
198                        content.sort();
199                        Some((work.data.clone(), content))
200                    }
201                }
202                WorkStatus::Done(_) => None,
203            })
204            .collect();
205
206        required_work.sort_by(|(a_data, a_content), (b_data, b_content)| {
207            match a_content.len().cmp(&b_content.len()) {
208                Ordering::Equal => a_data.source().cmp(b_data.source()),
209                other => other,
210            }
211        });
212
213        required_work.sort_by_key(|(_, content)| content.len());
214
215        Self::new(ErrorKind::CyclicWork {
216            work: required_work,
217        })
218    }
219
220    pub(crate) fn invalid_resource_path(
221        path: impl Into<String>,
222        message: impl Into<String>,
223    ) -> Self {
224        Self::new(ErrorKind::InvalidResourcePath {
225            location: path.into(),
226            message: message.into(),
227        })
228    }
229
230    pub(crate) fn require_unknown_resource(path: impl Into<PathBuf>) -> Self {
231        Self::new(ErrorKind::RequireUnknownResource { path: path.into() })
232    }
233
234    pub(crate) fn require_copied_resource(path: impl Into<PathBuf>) -> Self {
235        Self::new(ErrorKind::RequireCopiedResource { path: path.into() })
236    }
237
238    pub(crate) fn os_string_conversion(os_string: impl Into<OsString>) -> Self {
239        Self::new(ErrorKind::OsStringConversion {
240            os_string: os_string.into(),
241        })
242    }
243
244    pub(crate) fn invalid_glob_pattern(
245        pattern: impl Into<String>,
246        message: impl Into<String>,
247    ) -> Self {
248        Self::new(ErrorKind::InvalidGlobPattern {
249            pattern: pattern.into(),
250            message: message.into(),
251        })
252    }
253
254    /// Creates a custom error with the given message.
255    pub fn custom(message: impl Into<Cow<'static, str>>) -> Self {
256        Self::new(ErrorKind::Custom {
257            message: message.into(),
258        })
259    }
260}
261
262impl From<ResourceError> for DarkluaError {
263    fn from(err: ResourceError) -> Self {
264        match err {
265            ResourceError::NotFound(path) => DarkluaError::resource_not_found(path),
266            ResourceError::ExpectedUtf8 { path, utf8_error } => {
267                DarkluaError::expected_utf8(path, utf8_error)
268            }
269            ResourceError::IO { path, error } => DarkluaError::io_error(path, error),
270        }
271    }
272}
273
274impl From<json5::Error> for DarkluaError {
275    fn from(error: json5::Error) -> Self {
276        Self::new(ErrorKind::Deserialization {
277            message: error.to_string(),
278            data_type: "json",
279        })
280    }
281}
282
283impl From<serde_json::Error> for DarkluaError {
284    fn from(error: serde_json::Error) -> Self {
285        Self::new(ErrorKind::Deserialization {
286            message: error.to_string(),
287            data_type: "json",
288        })
289    }
290}
291
292impl From<serde_yaml::Error> for DarkluaError {
293    fn from(error: serde_yaml::Error) -> Self {
294        Self::new(ErrorKind::Deserialization {
295            message: error.to_string(),
296            data_type: "yaml",
297        })
298    }
299}
300
301impl From<toml::de::Error> for DarkluaError {
302    fn from(error: toml::de::Error) -> Self {
303        Self::new(ErrorKind::Deserialization {
304            message: error.to_string(),
305            data_type: "toml",
306        })
307    }
308}
309
310impl From<toml::ser::Error> for DarkluaError {
311    fn from(error: toml::ser::Error) -> Self {
312        Self::new(ErrorKind::Serialization {
313            message: error.to_string(),
314            data_type: "toml",
315        })
316    }
317}
318
319impl From<LuaSerializerError> for DarkluaError {
320    fn from(error: LuaSerializerError) -> Self {
321        Self::new(ErrorKind::Serialization {
322            message: error.to_string(),
323            data_type: "lua",
324        })
325    }
326}
327
328impl Display for DarkluaError {
329    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
330        match &*self.kind {
331            ErrorKind::Parser { path, error } => {
332                write!(f, "unable to parse `{}`: {}", path.display(), error)?;
333            }
334            ErrorKind::ResourceNotFound { path } => {
335                write!(f, "unable to find `{}`", path.display())?;
336            }
337            ErrorKind::ExpectedUtf8 { path, utf8_error } => {
338                write!(
339                    f,
340                    "unable to read `{}` as valid UTF-8: {}",
341                    path.display(),
342                    utf8_error
343                )?;
344            }
345            ErrorKind::InvalidConfiguration { path } => {
346                write!(f, "invalid configuration file at `{}`", path.display())?;
347            }
348            ErrorKind::MultipleConfigurationFound { paths } => {
349                write!(
350                    f,
351                    "multiple default configuration file found: {}",
352                    paths
353                        .iter()
354                        .map(|path| format!("`{}`", path.display()))
355                        .collect::<Vec<_>>()
356                        .join(", ")
357                )?;
358            }
359            ErrorKind::IO { path, error } => {
360                write!(f, "IO error with `{}`: {}", path.display(), error)?;
361            }
362            ErrorKind::UncachedWork { path } => {
363                write!(f, "attempt to obtain work at `{}`", path.display())?;
364            }
365            ErrorKind::RuleError {
366                path,
367                rule_name,
368                rule_number,
369                error,
370            } => {
371                if let Some(rule_number) = rule_number {
372                    write!(
373                        f,
374                        "error processing `{}` ({} [#{}]):{}{}",
375                        path.display(),
376                        rule_name,
377                        rule_number,
378                        if error.contains('\n') { '\n' } else { ' ' },
379                        error,
380                    )?;
381                } else {
382                    write!(
383                        f,
384                        "error processing `{}` ({}):{}{}",
385                        path.display(),
386                        rule_name,
387                        if error.contains('\n') { '\n' } else { ' ' },
388                        error,
389                    )?;
390                }
391            }
392            ErrorKind::CyclicWork { work } => {
393                const MAX_PRINTED_WORK: usize = 12;
394                const MAX_REQUIRED_PATH: usize = 20;
395
396                let total = work.len();
397                let list: Vec<_> = work
398                    .iter()
399                    .take(MAX_PRINTED_WORK)
400                    .map(|(data, required)| {
401                        let required_list: Vec<_> = required
402                            .iter()
403                            .take(MAX_REQUIRED_PATH)
404                            .map(|path| format!("      - {}", path.display()))
405                            .collect();
406
407                        format!(
408                            "    `{}` needs:\n{}",
409                            data.source().display(),
410                            required_list.join("\n")
411                        )
412                    })
413                    .collect();
414
415                write!(
416                    f,
417                    "cyclic work detected:\n{}{}",
418                    list.join("\n"),
419                    if total <= MAX_PRINTED_WORK {
420                        "".to_owned()
421                    } else {
422                        format!("\n    and {} more", total - MAX_PRINTED_WORK)
423                    }
424                )?;
425            }
426            ErrorKind::Deserialization { message, data_type } => {
427                write!(f, "unable to read {} data: {}", data_type, message)?;
428            }
429            ErrorKind::Serialization { message, data_type } => {
430                write!(f, "unable to serialize {} data: {}", data_type, message)?;
431            }
432            ErrorKind::InvalidResourcePath { location, message } => {
433                write!(
434                    f,
435                    "unable to require resource at `{}`: {}",
436                    location, message
437                )?;
438            }
439            ErrorKind::RequireUnknownResource { path } => {
440                write!(f, "unable to require unknown resource at `{}` (configure content loaders to load this file)", path.display())?;
441            }
442            ErrorKind::RequireCopiedResource { path } => {
443                write!(f, "unable to require copied resource at `{}` (configure content loaders to copy this file)", path.display())?;
444            }
445            ErrorKind::OsStringConversion { os_string } => {
446                write!(
447                    f,
448                    "unable to convert operating system string (`{}`) into a utf-8 string",
449                    os_string.to_string_lossy(),
450                )?;
451            }
452            ErrorKind::InvalidGlobPattern { pattern, message } => {
453                write!(f, "invalid glob pattern `{}`: {}", pattern, message)?;
454            }
455            ErrorKind::Custom { message } => {
456                write!(f, "{}", message)?;
457            }
458        };
459
460        if let Some(context) = &self.context {
461            write!(f, " ({})", context)?;
462        }
463
464        Ok(())
465    }
466}