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;