compatdb_core/settings.rs
1//! The settings layer: how a launcher fills a [`Rule`] from its own
2//! configuration, key by key and value by value as text, without knowing
3//! any key itself.
4//!
5//! A launcher reads the name of each setting with [`Key::from_setting`],
6//! branches on [`Key::kind`] and hands the value to [`Rule::set`] (one value
7//! of a matcher or a scalar, or one entry of a list), [`Rule::set_list`] (a
8//! whole list) or [`Rule::set_env`] (one environment entry). Every error is
9//! a [`FieldError`] whose `Display` is complete enough to print as it is,
10//! and whose [`FieldError::severity`] says whether the launcher warns and
11//! goes on or refuses the settings.
12//!
13//! The policy for settings that older launchers wrote lives here too: the
14//! retired keys, values and environment variables, and the refusal of
15//! `arguments_override` in a rule for every process
16//! ([`Key::allowed_on_any`]). So does what a launcher needs to check its
17//! configuration before it builds a rule: which keys a later layer of it
18//! replaces ([`Key::replaces_on_merge`]) and which environment names a rule
19//! refuses ([`check_env_name`]).
20
21use crate::{
22 Argument, DEFAULT_VALUE, DebugChannel, DllOverride, EnvEntry, EnvName, Exe, FieldError,
23 Fingerprint, Key, KeyKind, Rule, Scalar, ScalarValue, Setting, Severity,
24};
25
26/// The keys that are no longer read, each with why and what to do instead
27/// (the clause that follows the key in the message) and how a launcher
28/// treats it.
29const RETIRED_KEYS: &[(&str, &str, Severity)] = &[
30 (
31 "x87_sidecar",
32 "is no longer read: this Wine runs x87 code in FEX and has no x87 sidecar; remove it",
33 Severity::Warning,
34 ),
35 (
36 "cmdline_append",
37 "is no longer read; use arguments",
38 Severity::Error,
39 ),
40];
41
42/// The values that are no longer read, each with its key, why and what to
43/// do instead (the clause that follows the value in the message) and how a
44/// launcher treats it.
45const RETIRED_VALUES: &[(Key, &str, &str, Severity)] = &[(
46 Key::Dxgi,
47 "gptk",
48 "asks for Apple's D3DMetal, which this Wine does not have, so it is ignored as if it were \
49 not there: without another dxgi value the built-in rules decide, which give DXMT unless \
50 they pin the program to another tree; remove it",
51 Severity::Warning,
52)];
53
54/// The environment variables that a setting replaced, each with the key of
55/// that setting.
56const RETIRED_ENV: &[(&str, Key)] = &[
57 ("WINEDEBUG", Key::DebugChannels),
58 ("WINEDLLOVERRIDES", Key::DllOverrides),
59 ("WINE_CMDLINE_APPEND", Key::Arguments),
60 ("WINE_MAC_OPENGL", Key::OpenGl),
61 ("WINE_FORCE_NX", Key::ForceNx),
62];
63
64/// The setting names that are no longer read, each with how
65/// [`Key::from_setting`] treats it: the names it refuses with
66/// [`FieldError::RetiredKey`] and that severity.
67pub fn retired_keys() -> impl Iterator<Item = (&'static str, Severity)> {
68 RETIRED_KEYS
69 .iter()
70 .map(|&(name, _, severity)| (name, severity))
71}
72
73/// The values that are no longer read, each with its key and how
74/// [`Rule::set`] treats it: the values it refuses with
75/// [`FieldError::RetiredValue`] and that severity.
76pub fn retired_values() -> impl Iterator<Item = (Key, &'static str, Severity)> {
77 RETIRED_VALUES
78 .iter()
79 .map(|&(key, value, _, severity)| (key, value, severity))
80}
81
82/// The environment variables that a setting replaced, each with the key of
83/// that setting: the names for which [`retired_env`] gives that key and
84/// [`check_env_name`] refuses.
85pub fn retired_envs() -> impl Iterator<Item = (&'static str, Key)> {
86 RETIRED_ENV.iter().copied()
87}
88
89/// The setting that replaced the environment variable `name`, if one did.
90///
91/// The name is compared ASCII-case-insensitively: `compatdb.so` merges a
92/// rule's env entries into the Windows environment block by name without
93/// regard to case, so an entry for `WineDllOverrides` would replace the
94/// `WINEDLLOVERRIDES` there. [`Rule::set_env`] refuses such a variable; a
95/// launcher can check the environment it sets up itself the same way.
96///
97/// `WINEDEBUG` is among them: Wine reads it before `compatdb.so` loads, so a
98/// rule cannot set it, and `debug_channels` reaches it through
99/// [`Table::winedebug`](crate::Table::winedebug).
100#[must_use]
101pub fn retired_env(name: &str) -> Option<Key> {
102 RETIRED_ENV
103 .iter()
104 .find(|(retired, _)| retired.eq_ignore_ascii_case(name))
105 .map(|&(_, key)| key)
106}
107
108/// Check `name` as the name of a rule's environment entry, with the errors
109/// [`Rule::set_env`] gives for it.
110///
111/// A launcher runs it on the names of its own configuration, such as an
112/// `[env]` table, before it builds a rule from them, or on the environment
113/// it starts Wine with.
114///
115/// # Errors
116///
117/// [`FieldError::RetiredEnv`] for a variable that a setting replaced (see
118/// [`retired_env`]), and the errors of [`EnvName::new`]:
119/// [`FieldError::InvalidEnvName`] for an empty name or one with a `=`,
120/// [`FieldError::Nul`] for one with a NUL.
121pub fn check_env_name(name: &str) -> Result<(), FieldError> {
122 env_name(name)?;
123 Ok(())
124}
125
126/// `name` as the name of a rule's environment entry, or the error of
127/// [`check_env_name`].
128fn env_name(name: &str) -> Result<EnvName, FieldError> {
129 if let Some(key) = retired_env(name) {
130 return Err(FieldError::RetiredEnv {
131 name: name.to_owned(),
132 key,
133 });
134 }
135 EnvName::new(name)
136}
137
138/// The variables a launcher removes from the environment it starts Wine
139/// with, when it inherited them, so that the launch does not depend on
140/// where the launcher itself was started from.
141///
142/// - `WINEDLLOVERRIDES`: ntdll reads it for every module, so an inherited
143/// value would change the load order of the modules the rules'
144/// `dll_overrides` leave alone, and a rule's `mscoree,mshtml=default`
145/// would hand Mono and Gecko to it.
146/// - `WINE_LARGE_ADDRESS_AWARE`: it gives every 32-bit process the 4 GB
147/// address space, which `large_address_aware` decides per process.
148///
149/// `WINE_LARGE_ADDRESS_AWARE` is not retired, so a launcher may still set
150/// it for the whole launch on purpose. `WINEDLLOVERRIDES` is (see
151/// [`retired_env`]): a rule's `dll_overrides` does its job.
152#[must_use]
153pub const fn inherited_conflicts() -> &'static [&'static str] {
154 &["WINEDLLOVERRIDES", "WINE_LARGE_ADDRESS_AWARE"]
155}
156
157impl Key {
158 /// The key a launcher's setting called `name` names, compared
159 /// ASCII-case-insensitively as [`str::parse`] compares it, which this
160 /// is but for the keys that are no longer read.
161 ///
162 /// `dxgi` and `DXGI` therefore name the same key: a launcher that hands
163 /// both to [`Rule::set`] gets the value of the last one for a matcher or
164 /// a scalar, and the entries of both for a list.
165 ///
166 /// # Errors
167 ///
168 /// [`FieldError::RetiredKey`] for a key that is no longer read, with
169 /// its severity: `x87_sidecar` is a warning, since it asks for nothing
170 /// this Wine could still do, and `cmdline_append`, which `arguments`
171 /// replaced, an error. [`FieldError::UnknownKey`] for any other name
172 /// that is no key.
173 pub fn from_setting(name: &str) -> Result<Self, FieldError> {
174 if let Some(&(_, reason, severity)) = RETIRED_KEYS
175 .iter()
176 .find(|(retired, ..)| retired.eq_ignore_ascii_case(name))
177 {
178 return Err(FieldError::RetiredKey {
179 key: name.to_owned(),
180 reason,
181 severity,
182 });
183 }
184 name.parse()
185 }
186
187 /// Whether a rule for every process, one whose `exe` is `*`
188 /// ([`Exe::Any`]) with or without a fingerprint, may hold this setting.
189 ///
190 /// It may hold every scalar setting, every list and `env`, but not
191 /// `arguments_override`: for every process that would take away the
192 /// arguments the built-in rules give single programs. A matcher is not a
193 /// setting, so this is `false` for one: `exe` decides whether the rule
194 /// is for every process at all, and a fingerprint narrows the processes
195 /// it matches, so neither belongs among the settings a launcher offers
196 /// for every process.
197 ///
198 /// [`Rule::set`] and [`Rule::set_list`] refuse a setting for which this
199 /// is `false` in a `*` rule, and an `exe` of `*` in a rule that holds
200 /// one, with [`FieldError::ArgumentsOverrideOnAny`].
201 #[must_use]
202 pub const fn allowed_on_any(self) -> bool {
203 match self.kind() {
204 KeyKind::Matcher => false,
205 KeyKind::Scalar(_) | KeyKind::List | KeyKind::Env => {
206 !matches!(self, Self::ArgumentsOverride)
207 }
208 }
209 }
210
211 /// Whether a later layer's value for this key takes the place of an
212 /// earlier layer's, rather than adding to it, when a launcher builds one
213 /// rule from several layers of its configuration, such as a file of
214 /// defaults and a file per game laid over it.
215 ///
216 /// - A matcher or a scalar setting holds one value, so the later layer's
217 /// replaces it: `true`.
218 /// - A list adds the later layer's entries after the earlier layer's:
219 /// `false`.
220 /// - `arguments_override` is the exception among the lists: `true`. A
221 /// rule has one override, the whole list that replaces the arguments
222 /// of the rules folded before it (see [`Rule::arguments_override`]).
223 /// The entries of two layers added together would be an override
224 /// neither layer wrote, and an empty list in the later layer has to
225 /// clear an earlier layer's override, as `arguments_override=` alone
226 /// clears the arguments on the wire.
227 /// - `env` adds the later layer's entries: `false`. The entries fold in
228 /// order, so a later entry for the same variable wins.
229 ///
230 /// This is only about layers of one rule. Between rules nothing
231 /// replaces: every rule that matches a process applies, in the order
232 /// [`Table::resolve`](crate::Table::resolve) folds them.
233 #[must_use]
234 pub const fn replaces_on_merge(self) -> bool {
235 match self.kind() {
236 KeyKind::Matcher | KeyKind::Scalar(_) => true,
237 KeyKind::List => matches!(self, Self::ArgumentsOverride),
238 KeyKind::Env => false,
239 }
240 }
241}
242
243/// Whether a `*` rule refuses the field `key`: a setting that
244/// [`Key::allowed_on_any`] does not allow. A matcher is not a setting, and a
245/// `*` rule takes the fingerprints.
246const fn refused_on_any(key: Key) -> bool {
247 !matches!(key.kind(), KeyKind::Matcher) && !key.allowed_on_any()
248}
249
250impl Rule {
251 /// Set the field `key` from `value`, as a launcher's setting gives it:
252 /// the value of a matcher or a scalar setting, or one more entry of a
253 /// list. For a scalar, `default` in any case resets the setting (see
254 /// [`Setting::Reset`]); for `env`, `value` is `NAME=value`, as for
255 /// [`Rule::set_env`]. A `debug_channels` value is split at each `,` into
256 /// entries of their own, as `WINEDEBUG` reads it, so that
257 /// [`Table::winedebug`](crate::Table::winedebug) scopes each of them to
258 /// the rule's `exe`; an empty entry, `+relay,` included, is refused.
259 ///
260 /// On an error the rule is left as it was.
261 ///
262 /// # Errors
263 ///
264 /// The [`FieldError`] of a value the key does not take: a word the
265 /// setting does not know, a part that is empty or holds a NUL, and the
266 /// errors of [`Rule::set_env`]. A retired value is
267 /// [`FieldError::RetiredValue`]: `dxgi` `gptk`, a warning, since the
268 /// built-in rules then decide as if there were no value. A setting that
269 /// [`Key::allowed_on_any`] does not allow, `arguments_override`, in a
270 /// rule whose `exe` is `*`, or an `exe` of `*` in a rule that holds one,
271 /// is [`FieldError::ArgumentsOverrideOnAny`]: for every process it would
272 /// take away the arguments the built-in rules give single programs.
273 /// Make the rule with its `exe` first.
274 pub fn set(&mut self, key: Key, value: &str) -> Result<(), FieldError> {
275 self.refuse_on_any(key)?;
276 if let Some(scalar) = key.scalar() {
277 let setting = scalar_setting(scalar, value)?;
278 self.put_scalar(scalar, setting);
279 return Ok(());
280 }
281 match key {
282 Key::Exe => {
283 let exe = Exe::new(value)?;
284 if exe.is_any()
285 && Key::ALL
286 .iter()
287 .any(|&key| refused_on_any(key) && self.holds(key))
288 {
289 return Err(FieldError::ArgumentsOverrideOnAny);
290 }
291 self.exe = exe;
292 }
293 Key::Company => self.company = Some(Fingerprint::new(value)?),
294 Key::Product => self.product = Some(Fingerprint::new(value)?),
295 Key::OriginalFilename => self.original_filename = Some(Fingerprint::new(value)?),
296 Key::DllOverrides => self.dll_overrides.push(DllOverride::new(value)?),
297 Key::Arguments => self.arguments.push(Argument::new(value)?),
298 Key::ArgumentsOverride => {
299 let argument = Argument::new(value)?;
300 self.arguments_override
301 .get_or_insert_default()
302 .push(argument);
303 }
304 Key::DebugChannels => {
305 let channels = value
306 .split(',')
307 .map(DebugChannel::new)
308 .collect::<Result<Vec<_>, _>>()?;
309 self.debug_channels.extend(channels);
310 }
311 Key::Env => {
312 let (name, value) = value.split_once('=').unwrap_or((value, ""));
313 self.set_env(name, value)?;
314 }
315 // The scalar settings returned above.
316 Key::Dxgi
317 | Key::D3d9
318 | Key::Wined3d
319 | Key::OpenGl
320 | Key::DpiAware
321 | Key::LargeAddressAware
322 | Key::Gcmouse
323 | Key::ForceNx
324 | Key::MetalHud => {}
325 }
326 Ok(())
327 }
328
329 /// Make the list `key` hold `values`, each read as [`Rule::set`] reads
330 /// one entry, in place of the entries it had. For `arguments_override`
331 /// that sets the override, and no values clear the arguments of the
332 /// rules folded before (see [`Rule::arguments_override`]). For `env`
333 /// each value is `NAME=value`.
334 ///
335 /// On an error the rule is left as it was.
336 ///
337 /// # Errors
338 ///
339 /// [`FieldError::NotAList`] for a key that holds one value,
340 /// [`FieldError::ArgumentsOverrideOnAny`] as for [`Rule::set`], even
341 /// with no values, and the first error [`Rule::set`] gives for one of
342 /// the values.
343 pub fn set_list<T: AsRef<str>>(
344 &mut self,
345 key: Key,
346 values: impl IntoIterator<Item = T>,
347 ) -> Result<(), FieldError> {
348 let mut rule = self.clone();
349 match key {
350 Key::DllOverrides => rule.dll_overrides.clear(),
351 Key::Arguments => rule.arguments.clear(),
352 Key::DebugChannels => rule.debug_channels.clear(),
353 Key::Env => rule.env.clear(),
354 Key::ArgumentsOverride => rule.arguments_override = Some(Vec::new()),
355 _ => return Err(FieldError::NotAList(key)),
356 }
357 self.refuse_on_any(key)?;
358 for value in values {
359 rule.set(key, value.as_ref())?;
360 }
361 *self = rule;
362 Ok(())
363 }
364
365 /// Add the environment entry that sets `name` to `value` after the
366 /// rule's other entries; an empty `value` removes the variable.
367 ///
368 /// # Errors
369 ///
370 /// The errors of [`check_env_name`] for `name`, and of [`EnvEntry::new`]
371 /// for `value`.
372 pub fn set_env(&mut self, name: &str, value: &str) -> Result<(), FieldError> {
373 self.refuse_on_any(Key::Env)?;
374 let entry = EnvEntry::new(env_name(name)?, value)?;
375 self.env.push(entry);
376 Ok(())
377 }
378
379 /// [`FieldError::ArgumentsOverrideOnAny`] when the rule matches every
380 /// process by its `exe` and refuses the setting `key` there (see
381 /// [`Key::allowed_on_any`]).
382 const fn refuse_on_any(&self, key: Key) -> Result<(), FieldError> {
383 if self.exe.is_any() && refused_on_any(key) {
384 Err(FieldError::ArgumentsOverrideOnAny)
385 } else {
386 Ok(())
387 }
388 }
389
390 /// Whether the rule holds the setting `key`: a scalar setting set or
391 /// reset, a list or `env` with an entry, an `arguments_override` with or
392 /// without entries. `false` for a matcher, which is not a setting.
393 fn holds(&self, key: Key) -> bool {
394 match key {
395 Key::DllOverrides => !self.dll_overrides.is_empty(),
396 Key::ArgumentsOverride => self.arguments_override.is_some(),
397 Key::Arguments => !self.arguments.is_empty(),
398 Key::DebugChannels => !self.debug_channels.is_empty(),
399 Key::Env => !self.env.is_empty(),
400 Key::Exe | Key::Company | Key::Product | Key::OriginalFilename => false,
401 Key::Dxgi
402 | Key::D3d9
403 | Key::Wined3d
404 | Key::OpenGl
405 | Key::DpiAware
406 | Key::LargeAddressAware
407 | Key::Gcmouse
408 | Key::ForceNx
409 | Key::MetalHud => key
410 .scalar()
411 .is_some_and(|scalar| !self.scalar(scalar).is_unset()),
412 }
413 }
414}
415
416/// What `text` says about the scalar setting `scalar`: `default`, in any
417/// case, resets it, and anything else is a value of it, or a retired one.
418fn scalar_setting(scalar: Scalar, text: &str) -> Result<Setting<ScalarValue>, FieldError> {
419 if text.eq_ignore_ascii_case(DEFAULT_VALUE) {
420 return Ok(Setting::Reset);
421 }
422 if let Some(&(key, _, reason, severity)) = RETIRED_VALUES
423 .iter()
424 .find(|(key, value, ..)| *key == scalar.key() && value.eq_ignore_ascii_case(text))
425 {
426 return Err(FieldError::RetiredValue {
427 key,
428 value: text.to_owned(),
429 reason,
430 severity,
431 });
432 }
433 scalar.parse_value(text).map(Setting::Set)
434}
435
436#[cfg(test)]
437mod tests;