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;