Skip to main content

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