Skip to main content

compatdb_core/
error.rs

1//! What can be wrong with one field of a rule, and with a line of
2//! [`ENV_VAR`] or of the file [`FILE_ENV_VAR`] names.
3
4use std::fmt;
5
6#[cfg(doc)]
7use crate::{ENV_VAR, FILE_ENV_VAR, Rule, Table, retired_env};
8use crate::{Key, RulesText};
9
10/// Why a field of a rule cannot be read, or a value cannot be set.
11///
12/// The [`Display`](fmt::Display) form is a short phrase, for example
13/// `unknown key "bogus"`, which `compatdb.so` logs after `ignoring`.
14#[non_exhaustive]
15#[derive(Clone, Debug, PartialEq, Eq)]
16pub enum FieldError {
17    /// A key the format does not know, as it was written.
18    UnknownKey(String),
19    /// A value that `key` does not take.
20    ///
21    /// From a field of a rule and from
22    /// [`Scalar::parse_value`](crate::Scalar::parse_value), `expected` is
23    /// [`Key::values`], `default` included. From the `FromStr` of a value
24    /// type such as [`Dxgi`](crate::Dxgi), it lists that type's values
25    /// alone, since `default` is none of them.
26    BadValue {
27        /// The key whose value it is.
28        key: Key,
29        /// The value as it was written, decoded.
30        value: String,
31        /// The spellings the value can have there, in lowercase.
32        expected: &'static [&'static str],
33    },
34    /// A NUL in a value, which the process APIs that take it would end the
35    /// text at.
36    Nul,
37    /// An `exe` that is empty, or a rule without one, which matches no
38    /// process.
39    MissingExe,
40    /// An `exe` that starts with `steam:` and has no app id after it, as it
41    /// was written (see [`Exe::new`](crate::Exe::new)).
42    BadSteamApp(String),
43    /// An empty argument, which would append nothing.
44    EmptyArgument,
45    /// An empty `debug_channels` entry, which names no channel.
46    EmptyDebugChannel,
47    /// A `debug_channels` entry with a `,`, as it was written, which
48    /// `WINEDEBUG` would read as more than one entry.
49    CommaInDebugChannel(String),
50    /// A variable name that is empty or holds a `=`, as it was written.
51    InvalidEnvName(String),
52    /// A DLL override without the `=` between its modules and its load
53    /// order, as it was written.
54    InvalidDllOverride(String),
55    /// A `%` in a value that two hex digits do not follow.
56    BadEscape,
57    /// A value whose escapes decode to bytes that are not UTF-8.
58    InvalidUtf8,
59    /// A field without the `=` between its key and its value.
60    MissingEquals,
61    /// A second field for a key that holds one value: a matcher or a scalar
62    /// setting.
63    Duplicate(Key),
64    /// A key that the settings of a launcher may still hold but that is no
65    /// longer read (see [`Key::from_setting`]).
66    RetiredKey {
67        /// The key as it was written.
68        key: String,
69        /// Why it is no longer read and what to do instead, a clause that
70        /// follows the key.
71        reason: &'static str,
72        /// Whether a launcher ignores it or refuses it.
73        severity: Severity,
74    },
75    /// A value of `key` that is no longer read (see [`Rule::set`]).
76    RetiredValue {
77        /// The key whose value it is.
78        key: Key,
79        /// The value as it was written.
80        value: String,
81        /// Why it is no longer read and what to do instead, a clause that
82        /// follows the value.
83        reason: &'static str,
84        /// Whether a launcher ignores it or refuses it.
85        severity: Severity,
86    },
87    /// An environment variable that a setting replaced, as it was written,
88    /// with the key of that setting (see [`retired_env`]).
89    RetiredEnv {
90        /// The variable as it was written.
91        name: String,
92        /// The setting that replaced it.
93        key: Key,
94    },
95    /// `arguments_override` in a rule whose `exe` is `*` (see [`Rule::set`]).
96    ArgumentsOverrideOnAny,
97    /// A setting that a rule whose `exe` is a Steam app id refuses (see
98    /// [`Rule::set`]).
99    RefusedOnSteamApp(Key),
100    /// A list of values for a key that holds one (see [`Rule::set_list`]).
101    NotAList(Key),
102}
103
104/// How a launcher treats a [`FieldError`] from its own settings, as
105/// [`FieldError::severity`] gives it.
106#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
107pub enum Severity {
108    /// The value is ignored, the rule is left as it was, and the launcher
109    /// prints the error as a warning and goes on.
110    Warning,
111    /// The launcher refuses the settings: what they ask for would not
112    /// happen.
113    Error,
114}
115
116impl FieldError {
117    /// How a launcher treats this error from [`Rule::set`],
118    /// [`Rule::set_list`], [`Rule::set_env`] or [`Key::from_setting`]:
119    /// [`Severity::Warning`] for a retired key or value that asks for
120    /// nothing this Wine could still do, [`Severity::Error`] for everything
121    /// else.
122    ///
123    /// [`Table::parse_lenient`] ignores every field it cannot read, whatever
124    /// its severity.
125    #[must_use]
126    pub const fn severity(&self) -> Severity {
127        match self {
128            Self::RetiredKey { severity, .. } | Self::RetiredValue { severity, .. } => *severity,
129            _ => Severity::Error,
130        }
131    }
132}
133
134impl fmt::Display for FieldError {
135    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136        match self {
137            Self::UnknownKey(key) => write!(f, "unknown key {key:?}"),
138            Self::BadValue { key, value, .. } => write!(f, "unknown {key} value {value:?}"),
139            Self::Nul => f.write_str("a NUL in its value"),
140            Self::MissingExe => f.write_str("no exe"),
141            Self::BadSteamApp(text) => write!(f, "invalid Steam app id {text:?}"),
142            Self::EmptyArgument => f.write_str("an empty argument"),
143            Self::EmptyDebugChannel => f.write_str("an empty debug channel"),
144            Self::CommaInDebugChannel(text) => write!(f, "debug channel {text:?} with a ','"),
145            Self::InvalidEnvName(name) => write!(f, "invalid variable name {name:?}"),
146            Self::InvalidDllOverride(text) => {
147                write!(f, "DLL override {text:?} without '=' before its load order")
148            }
149            Self::BadEscape => f.write_str("a '%' without two hex digits after it"),
150            Self::InvalidUtf8 => f.write_str("escapes that are not UTF-8"),
151            Self::MissingEquals => f.write_str("a field without '='"),
152            Self::Duplicate(key) => write!(f, "{key} given more than once"),
153            Self::RetiredKey { key, reason, .. } => write!(f, "{key} {reason}"),
154            Self::RetiredValue {
155                key, value, reason, ..
156            } => write!(f, "{key} value {value:?} {reason}"),
157            Self::RetiredEnv { name, key } => {
158                write!(f, "env {name} is no longer read; use the {key} setting")
159            }
160            Self::ArgumentsOverrideOnAny => f.write_str(
161                "arguments_override needs an exe other than *: for every process it would also \
162                 take away the arguments the built-in rules give single programs, which some of \
163                 them need to work",
164            ),
165            Self::RefusedOnSteamApp(Key::DebugChannels) => f.write_str(
166                "debug_channels needs an exe that names a program: WINEDEBUG scopes an entry by \
167                 the image name, which a Steam app id does not give",
168            ),
169            Self::RefusedOnSteamApp(key) => write!(
170                f,
171                "{key} needs an exe that names a program: a Steam app id can match more than one \
172                 program, and it would take away the arguments the built-in rules give them, \
173                 which some of them need to work"
174            ),
175            Self::NotAList(key) => write!(f, "{key} holds one value, not a list"),
176        }
177    }
178}
179
180impl std::error::Error for FieldError {}
181
182/// One problem with the value of [`ENV_VAR`] or the file [`FILE_ENV_VAR`]
183/// names: the text and the line, the field as it was written, if the problem
184/// is with one, and what is wrong.
185///
186/// Its [`Display`](fmt::Display) form is the warning `compatdb.so` logs, for
187/// example `WINE_COMPATDB line 2: ignoring unknown key "bogus"`, or
188/// `WINE_COMPATDB_FILE line 2: ...` for the file, which also says what
189/// [`Table::parse_lenient`] or [`Table::parse_file_lenient`] did about it.
190#[derive(Clone, Debug, PartialEq, Eq)]
191pub struct Diagnostic {
192    /// The text the line is in.
193    pub text: RulesText,
194    /// The line, counted from 1, blank lines and a legacy header included.
195    pub line: usize,
196    /// The field as it was written, or `None` for a problem with the whole
197    /// line.
198    pub field: Option<String>,
199    /// What is wrong.
200    pub error: FieldError,
201}
202
203impl fmt::Display for Diagnostic {
204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205        let field = self.field.as_deref().unwrap_or_default();
206        write!(f, "{} line {}: ", self.text.name(), self.line)?;
207        match &self.error {
208            FieldError::MissingExe => f.write_str("ignoring the line, it has no exe"),
209            FieldError::MissingEquals => write!(f, "ignoring field {field:?} without '='"),
210            FieldError::Duplicate(key) => {
211                write!(f, "{key} given more than once, the last one applies")
212            }
213            FieldError::BadEscape => write!(
214                f,
215                "field {field:?} has {}, which is kept as written",
216                self.error
217            ),
218            FieldError::Nul
219            | FieldError::InvalidUtf8
220            | FieldError::EmptyArgument
221            | FieldError::EmptyDebugChannel => {
222                write!(f, "ignoring field {field:?}: {}", self.error)
223            }
224            error => write!(f, "ignoring {error}"),
225        }
226    }
227}
228
229impl std::error::Error for Diagnostic {
230    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
231        Some(&self.error)
232    }
233}
234
235/// Why [`Table::parse_strict`] refused a value of [`ENV_VAR`]: every
236/// problem it found, in line order.
237#[derive(Clone, Debug, PartialEq, Eq)]
238pub struct ParseError(pub(crate) Vec<Diagnostic>);
239
240impl ParseError {
241    /// Every problem, in line order; never empty.
242    #[must_use]
243    pub fn diagnostics(&self) -> &[Diagnostic] {
244        &self.0
245    }
246}
247
248impl fmt::Display for ParseError {
249    /// The diagnostics, one per line.
250    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251        for (i, diagnostic) in self.0.iter().enumerate() {
252            if i > 0 {
253                f.write_str("\n")?;
254            }
255            write!(f, "{diagnostic}")?;
256        }
257        Ok(())
258    }
259}
260
261impl std::error::Error for ParseError {}
262
263#[cfg(test)]
264mod tests;