compatdb_core/lib.rs
1//! The per-process rules of this Wine's `compatdb.so`, and the wire format
2//! a launcher hands them over in.
3//!
4//! `compatdb.so` is the unix library Wine's ntdll loads into every process
5//! of the bundle before any PE code runs. It reads the process's image
6//! name, version resource and Steam app id, takes the built-in rules of
7//! [`builtin`], appends the rules a launcher passed in the environment variable
8//! [`ENV_VAR`] (`WINE_COMPATDB`), then those of the file [`FILE_ENV_VAR`]
9//! names, folds every rule that matches the process into one [`Resolution`] and
10//! applies it: the Direct3D trees to load, the renderer of wined3d, which
11//! OpenGL the Mac driver gives, DLL load-order overrides, the DPI awareness,
12//! large address awareness, whether the Mac driver reads raw mouse input
13//! from `GameController`, whether no-execute stays on, whether the Metal
14//! performance HUD shows, command-line arguments and environment entries.
15//! It logs every value with the rule it came from: the lines on Wine's
16//! stderr that start with `compatdb:`, which
17//! [docs/compatdb.md](https://github.com/athei/mach9/blob/main/docs/compatdb.md#logging-and-verification)
18//! describes, show a launcher author what a process got and why.
19//!
20//! This crate is the only place that knows the rules and their format. A
21//! launcher uses it to build its rules ([`Rule`], [`Table`]), to write the
22//! value of [`ENV_VAR`] (`Table`'s `Display`) and to show what a program
23//! would get ([`Table::preview`]). `compatdb.so` uses the same crate to
24//! read the value ([`Table::parse_lenient`]) and the file
25//! ([`Table::parse_file_lenient`]) and resolve them ([`Table::resolve`]);
26//! the [`runtime`] module holds what only it needs.
27//!
28//! Everything here is pure: it works on `&str`, `&[u16]` (UTF-16 as Wine
29//! hands it over) and owned values, with no unsafe and no I/O, so the
30//! decision logic is unit-tested on the host and the `.so` itself only has
31//! to move bytes in and out of the process.
32//!
33//! # Quick start
34//!
35//! A launcher builds one rule for every process from its launch-wide
36//! settings and one per program it knows something about, writes the
37//! table and starts Wine with it in [`ENV_VAR`]. The whole process tree
38//! inherits the variable, so a rule for a program the game starts reaches
39//! it too. The value is fixed when the tree starts, so a game started by a
40//! program that is already running, such as a store client, gets that
41//! program's value. For rules that change while a tree runs, a launcher adds
42//! a file with [`FILE_ENV_VAR`], see [Format](#format).
43//!
44//! The settings layer fills a rule from text without the launcher knowing
45//! any key: [`Key::from_setting`] names the key, [`Key::kind`] says whether
46//! it holds one value, a list or environment entries, and [`Rule::set`],
47//! [`Rule::set_list`] or [`Rule::set_env`] take the value. Every error is a
48//! [`FieldError`] to print as it is, and [`FieldError::severity`] says
49//! whether to warn and go on or to refuse the settings.
50//!
51//! ```
52//! use std::process::Command;
53//!
54//! use compatdb_core::{
55//! Argument, Dxgi, ENV_VAR, Exe, Fingerprint, Key, Rule, Severity, Table, VersionInfo,
56//! };
57//!
58//! // The launch-wide settings, as the launcher's own config holds them.
59//! let mut every = Rule::new(Exe::Any);
60//! for (name, value) in [("dpi_aware", "false"), ("GCMouse", "Default")] {
61//! every.set(Key::from_setting(name)?, value)?;
62//! }
63//!
64//! // A rule for one game, from typed parts.
65//! let game = Rule::new(Exe::new("Game.exe")?)
66//! .with_company(Fingerprint::new("Some Vendor")?)
67//! .with_dxgi(Dxgi::Wined3d)
68//! .with_argument(Argument::new("-windowed")?);
69//!
70//! // A retired value is a warning: the rule stays as it was.
71//! let mut old = Rule::new(Exe::new("Old.exe")?);
72//! let error = old.set(Key::Dxgi, "gptk").err().ok_or("gptk was taken")?;
73//! assert_eq!(error.severity(), Severity::Warning);
74//! assert!(old.sets_nothing());
75//!
76//! let mut table = Table::new();
77//! table.push(every);
78//! table.push(game);
79//! let value = table.to_string();
80//! assert_eq!(
81//! value,
82//! "exe=*;dpi_aware=false;gcmouse=default\n\
83//! exe=Game.exe;company=Some Vendor;dxgi=wined3d;arguments=-windowed"
84//! );
85//! let mut wine = Command::new("bin/wine");
86//! wine.env(ENV_VAR, &value);
87//!
88//! // What that game would get, the built-in rules included.
89//! let version = VersionInfo {
90//! company: "Some Vendor Ltd.".into(),
91//! ..VersionInfo::default()
92//! };
93//! let resolution = table.preview("Game.exe", &version);
94//! assert_eq!(resolution.dxgi(), Some(Dxgi::Wined3d));
95//! assert_eq!(resolution.dpi_aware(), Some(false));
96//! // `default` hands gcmouse back to upstream Wine.
97//! assert_eq!(resolution.gcmouse(), None);
98//! assert_eq!(resolution.arguments, ["-windowed"]);
99//! # Ok::<(), Box<dyn std::error::Error>>(())
100//! ```
101//!
102//! # Format
103//!
104//! The value of [`ENV_VAR`] is one record per line, each a rule:
105//!
106//! ```text
107//! exe=Game.exe;company=Some Vendor;d3d9=wined3d;dpi_aware=false;env=VKD3D_CONFIG=a%3Bb
108//! exe=OldGame.exe;large_address_aware=true
109//! exe=*;gcmouse=false
110//! ```
111//!
112//! - A line ends at LF, and a CR before the LF is dropped. A line of
113//! whitespace alone is skipped. Lines are counted from 1, blank lines
114//! included; that number is the rule's [`Source::Line`], and the line
115//! of each [`Diagnostic`]. The file below counts its own lines.
116//! - A record is fields joined by `;`. An empty field is skipped.
117//! - A field is `key=value`, and the key ends at the first `=`. The keys
118//! are those of [`Key`].
119//! - Every record needs an `exe`. The other matchers, `company`, `product`
120//! and `original_filename`, are optional (see [Matching](#matching)).
121//! A matcher or a scalar setting holds one value, and a list takes one
122//! entry per field, in order.
123//! - A scalar setting takes one of the spellings of [`Key::values`]: one
124//! of its words, or `default`, which resets it ([`Setting::Reset`]).
125//! - `arguments_override` with an empty value sets the override without
126//! adding an entry, so `arguments_override=` alone clears the arguments.
127//! An empty entry of `arguments` or `debug_channels` is refused.
128//! - An `env` field is `env=NAME=value`. The name ends at the first `=`,
129//! so a name cannot hold one ([`EnvName`]); the value may. An empty
130//! value, or none, removes the variable.
131//! - What each list entry and matcher may hold is the type of its part:
132//! [`Exe`], [`Fingerprint`], [`DllOverride`] (needs a `=`), [`Argument`],
133//! [`DebugChannel`] (no `,`) and [`EnvName`]. None of them holds a NUL.
134//!
135//! Escaping. In a value, `Display` writes `%`, `;` and every ASCII control
136//! character (U+0000 to U+001F and U+007F, CR and LF among them) as `%XX`,
137//! the uppercase hex of the byte. Everything else, `=`, `,`, spaces and
138//! non-ASCII included, is written as it is, in UTF-8. Reading decodes every
139//! `%XX`, in either case, to its byte, and the bytes of a value must be
140//! UTF-8 without a NUL: `%C3%A9` is `é`, and `%FF` or `%00` drops the
141//! field. A `%` that two hex digits do not follow is kept as it is, with a
142//! diagnostic.
143//!
144//! The case rule. Every word the format defines is read
145//! ASCII-case-insensitively and written in lowercase: the keys, the words
146//! of the scalar settings, `true`, `false`, `default`, the `steam:` of an
147//! `exe` and the `v` of a legacy header. User data is kept byte for byte: the
148//! `exe`, the fingerprint values, list entries and env names and values. So
149//! `DXGI=WineD3D` reads as `dxgi=wined3d`, and `exe=STEAM:440` as
150//! `exe=steam:440`, while `exe=Game.EXE` keeps its case (matching ignores it,
151//! see [Matching](#matching)).
152//!
153//! There is no format version. A first line of `v=` and ASCII digits, the
154//! header launchers wrote for an older format, is skipped without a
155//! diagnostic; it still counts as a line. Compatibility is per field
156//! instead: `compatdb.so` ignores a field whose key it does not know or
157//! whose value its key does not take, with a warning, and applies the rest
158//! of the record. A key added later is therefore only ever a setting: the
159//! matcher keys are a closed set, since a library that ignored an unknown
160//! matcher would apply the rule to more processes than it names. So a Steam
161//! app is a value of `exe`, `steam:<appid>`, and no key of its own: a library
162//! from before it reads the value as a basename, which no process has, and
163//! the rule matches nothing, where `exe=*;steam_app=440` would apply to every
164//! process. The `name` and `enabled` keys of the older format, where a line
165//! changed or dropped a built-in rule by its name, are unknown keys like any
166//! other, and a `compatdb.so` from before that change drops every line
167//! without `name`, so a table in this format does nothing on an older bundle.
168//!
169//! `Display` writes the canonical text: one line per rule and no line
170//! break after the last, the fields in the order of [`Key::ALL`], every
171//! word in lowercase, and none of the built-in rules, which `compatdb.so`
172//! has compiled in.
173//!
174//! [`FILE_ENV_VAR`] (`WINE_COMPATDB_FILE`) adds rules in a file: the variable
175//! holds the path and each process reads the file when it starts, so a
176//! process started later in a running tree gets the rules the file holds by
177//! then. The table of `compatdb.so` is the built-in rules, then the rules of
178//! [`ENV_VAR`], then the rules of the file, so a launcher may send its own
179//! rules in [`ENV_VAR`] and point a stable file at the rules that change.
180//!
181//! - The file's text is what [`ENV_VAR`] would hold, and it is read, matched
182//! and folded the same way ([Folding](#folding)). Only `debug_channels`
183//! differs, see [Launcher responsibilities](#launcher-responsibilities).
184//! - The file counts its own lines. A rule of the file is a
185//! [`Source::FileLine`] and [`Table::parse_file_lenient`] reads it, so the
186//! log calls its rules and problems `WINE_COMPATDB_FILE line <n>`, and
187//! those of the variable stay `WINE_COMPATDB line <n>`.
188//! - An empty [`FILE_ENV_VAR`] counts as unset.
189//! - The path must be absolute and name a regular file within the size
190//! limit (`MAX_BYTES` in
191//! [`rules_file.rs`](https://github.com/athei/mach9/blob/main/compatdb/src/rules_file.rs)).
192//! Bytes that are not UTF-8 are replaced, as in the variable.
193//! - A file that cannot be used adds no rules, with a warning. The rules of
194//! [`ENV_VAR`] and the built-in rules still apply.
195//!
196//! ## Strict and lenient parsing
197//!
198//! [`Table::parse_lenient`] reads the value the way `compatdb.so` does,
199//! which must never abort a process. Each problem is a [`Diagnostic`],
200//! whose `Display` is the warning the library logs:
201//!
202//! - A field with an unknown key, a value its key or its part does not
203//! take, no `=`, escapes that are not UTF-8, or a NUL is left out, and
204//! the rest of its line still applies.
205//! - A line without an `exe` is left out as a whole, since it matches no
206//! process.
207//! - A second field for a matcher or a scalar setting replaces the first.
208//! - A `%` that two hex digits do not follow is kept as it is.
209//!
210//! [`Table::parse_strict`], which is also `Table`'s `FromStr`, refuses
211//! every problem lenient parsing reports, with a [`ParseError`] that lists
212//! them. A launcher that writes the value with `Display` never needs to
213//! read it; strict parsing is for text a person wrote.
214//!
215//! Two guarantees hold, and the [round-trip suite] checks them on
216//! generated tables: a table built with [`Table::new`] and [`Table::push`]
217//! reads back from its text as itself, and canonical text writes back byte
218//! for byte.
219//!
220//! [round-trip suite]: https://github.com/athei/mach9/blob/main/compatdb/core/tests/roundtrip.rs
221//!
222//! ```
223//! use compatdb_core::Table;
224//!
225//! let table: Table = "EXE=Game.exe;DPI_Aware=False;env=VKD3D_CONFIG=a%3Bb".parse()?;
226//! let text = table.to_string();
227//! assert_eq!(text, "exe=Game.exe;dpi_aware=false;env=VKD3D_CONFIG=a%3Bb");
228//! assert_eq!(Table::parse_strict(&text)?, table);
229//! assert_eq!(Table::parse_strict(&text)?.to_string(), text);
230//!
231//! // Lenient parsing keeps what it can read and says what it left out.
232//! let (table, diagnostics) = Table::parse_lenient("exe=Game.exe;bogus=1;gcmouse=yes");
233//! assert_eq!(table.rules().len(), 1);
234//! let warnings: Vec<String> = diagnostics.iter().map(ToString::to_string).collect();
235//! assert_eq!(
236//! warnings,
237//! [
238//! "WINE_COMPATDB line 1: ignoring unknown key \"bogus\"",
239//! "WINE_COMPATDB line 1: ignoring unknown gcmouse value \"yes\"",
240//! ]
241//! );
242//! assert!(Table::parse_strict("exe=Game.exe;bogus=1").is_err());
243//! # Ok::<(), Box<dyn std::error::Error>>(())
244//! ```
245//!
246//! # Matching
247//!
248//! A rule has no name: it is its matchers and its settings
249//! ([`Rule::matches`]).
250//!
251//! - `exe` matches the image basename ASCII-case-insensitively, or every
252//! process when it is `*` ([`Exe::Any`]).
253//! - `exe=steam:<appid>` ([`Exe::Steam`]) matches a process by its Steam app
254//! id instead, whatever its image name, for a launcher that drives
255//! `steam.exe -applaunch <appid>` and knows the id but not the executable.
256//! The id is the decimal value of `SteamAppId` in the process's environment
257//! block ([`runtime::steam_app`]). A value that is empty, 0 or no number is
258//! no id, so Steam's own processes, which have the variable empty, do not
259//! match. A game started outside Steam has no id unless whoever started it
260//! set the variable, so the rule does not match it, and neither does
261//! [`Table::preview`], which knows no id. [`Exe::new`] refuses a `steam:`
262//! that no id from 1 to [`u32::MAX`] follows.
263//! - `company`, `product` and `original_filename` are optional
264//! ASCII-case-insensitive substrings of the image's version strings
265//! `CompanyName`, `ProductName` and `OriginalFilename` ([`VersionInfo`]).
266//! A string the image lacks reads as empty, so a rule that pins a value
267//! for it does not match. They combine with a Steam app id as with a
268//! basename: `exe=steam:440;product=Foo`.
269//! - A rule whose `exe` matches but whose fingerprint does not does not
270//! apply; nothing falls back to it, and the other rules that match still
271//! apply.
272//! - [`Table::duplicate_matchers`] finds rules that match exactly the same
273//! processes, which in a hand-written table is a mistake.
274//! - A process has its app id from its environment block, which it inherits,
275//! so a rule for an app id can match more than one program. [`Rule::set`]
276//! therefore refuses in such a rule what it refuses in a `*` rule
277//! ([`Key::allowed_on_any`]), and `debug_channels`, since `WINEDEBUG` scopes
278//! an entry by the image name ([`Table::winedebug`] leaves one out). Reading
279//! text refuses neither, [`Table::parse_strict`] included: both stay in the
280//! rule and its text, and the rule's `arguments_override` is ignored and its
281//! `debug_channels` are only logged.
282//!
283//! # Folding
284//!
285//! Every rule that matches a process applies; no rule removes or replaces
286//! another. [`Table::resolve`] folds them in this order, and within each
287//! group a rule without a fingerprint before one with, otherwise in table
288//! order (line order for [`ENV_VAR`], then line order for the file
289//! [`FILE_ENV_VAR`] names):
290//!
291//! 1. the built-in `*` rules;
292//! 2. the `*` rules of [`ENV_VAR`] and the file;
293//! 3. the built-in rules that name an executable;
294//! 4. the rules of [`ENV_VAR`] and the file that name an executable.
295//!
296//! The variable and the file are one place in this order, so only table
297//! order tells them apart. A rule of the file beats a rule of [`ENV_VAR`] of
298//! equal specificity, because it is declared later. A `*` rule of the file
299//! does not beat a rule of [`ENV_VAR`] that names an executable, and a rule
300//! of the file without a fingerprint does not beat one of [`ENV_VAR`] with a
301//! fingerprint.
302//!
303//! A rule for a Steam app id counts as naming an executable: it adds no group
304//! to this order, and within one folds in table order with the rules for a
305//! basename ([`Specificity::Exe`], [`Specificity::ExeWithFingerprint`]). So it
306//! beats a built-in rule for a program in every process that holds the id,
307//! as a rule of the launcher for that program does: `exe=steam:12210;dxgi=dxmt`
308//! moves a Rockstar `Launcher.exe` that holds the id off the tree its built-in
309//! rule pins, and `exe=steam:12210;metal_hud=true` undoes the
310//! `MTL_HUD_ENABLED=0` of such a `SocialClubHelper.exe`. A fingerprint narrows
311//! the rule to the game: `exe=steam:12210;product=Grand Theft Auto IV;dxgi=dxmt`.
312//!
313//! So a launch-wide `*` rule replaces a built-in default but not what a
314//! built-in rule for one program sets: that fix survives launch-wide
315//! settings, and a rule of the launcher for that program changes it, with
316//! or without a fingerprint. Within the caller's rules, a rule for an
317//! executable beats a `*` rule whatever their line order.
318//!
319//! - A scalar setting ends up with the value of the last rule that sets it.
320//! `default` resets it: from that rule on it is unset, as if no rule had
321//! set it, so upstream Wine decides unless a rule folded later sets it
322//! again. For most settings that is the same as one of the values, but
323//! not for `dpi_aware`: `false` makes a program unaware, `default` leaves
324//! it to the registry and the manifest.
325//! - The lists accumulate in fold order.
326//! - `arguments_override` replaces the arguments the rules folded before it
327//! gathered, the built-in ones included, with its own entries; the
328//! rule's own `arguments` and those of the rules folded after it still
329//! append. In a rule for a Steam app id it is ignored
330//! ([`Rule::arguments_override`]), and the rule's `arguments` still append.
331//! - A `dll_overrides` entry with the order `default`, such as
332//! `mscoree,mshtml=default`, takes its modules out of the entries the
333//! rules folded before it gave them and adds none, so the registry and
334//! `WINEDLLOVERRIDES` decide for them again.
335//! - The `env` entries of a built-in `*` rule without a fingerprint are
336//! defaults: one sets its variable only where the process does not have
337//! it, so a value whoever started the process exported wins, an empty
338//! one included.
339//! Every other entry, a `*` rule's of [`ENV_VAR`] or the file included,
340//! replaces the variable, and an entry without a value removes it.
341//! [`Resolution::env_steps`] says what each entry does in a process.
342//! - `wined3d` and `metal_hud` become env entries too
343//! ([`Resolution::fold_derived_env`]): `wined3d` the `WINE_D3D_CONFIG`
344//! after every rule's own entries, `metal_hud` the `MTL_HUD_` variables
345//! where the rule that set it folds, before its own entries, so an `env`
346//! entry for one of them from that rule or a later one wins.
347//! - A program inherits the environment of the process that started it,
348//! the values `compatdb.so` set there included. A value a rule for one
349//! program sets for a variable with a `*` default therefore carries into
350//! the programs it starts, whose default finds the variable set. A
351//! value it removes does not: the default sets it again in the child.
352//! The settings ntdll stores for the process (`dpi_aware`,
353//! `large_address_aware`, `gcmouse`, `force_nx`, `opengl`) are its own:
354//! a child gets the value of its own rules.
355//!
356//! [`Resolution`] keeps where each value came from ([`Origin`],
357//! [`ScalarState`]), which `compatdb.so` logs.
358//!
359//! ```
360//! use compatdb_core::{Dxgi, Exe, Origin, Rule, Scalar, Table, VersionInfo};
361//!
362//! let mut table = Table::new();
363//! table.push(Rule::new(Exe::Any).with_dxgi(Dxgi::Wined3d));
364//!
365//! // A game gets the launch-wide value over the built-in default.
366//! let game = table.preview("Game.exe", &VersionInfo::default());
367//! assert_eq!(game.dxgi(), Some(Dxgi::Wined3d));
368//! assert_eq!(game.scalar(Scalar::Dxgi).set_by, Some(Origin::Line(1)));
369//!
370//! // Steam's web helper keeps the DXMT its built-in rule pins.
371//! let helper = VersionInfo {
372//! company: "Valve Corporation".into(),
373//! product: "Steam Client WebHelper".into(),
374//! ..VersionInfo::default()
375//! };
376//! let steam = table.preview("steamwebhelper.exe", &helper);
377//! assert_eq!(steam.dxgi(), Some(Dxgi::Dxmt));
378//! assert!(matches!(
379//! steam.scalar(Scalar::Dxgi).set_by,
380//! Some(Origin::BuiltInFor(_))
381//! ));
382//!
383//! // A rule of the launcher for the helper changes it.
384//! table.push(Rule::new(Exe::new("steamwebhelper.exe")?).with_dxgi(Dxgi::Wined3d));
385//! let steam = table.preview("steamwebhelper.exe", &helper);
386//! assert_eq!(steam.dxgi(), Some(Dxgi::Wined3d));
387//! # Ok::<(), Box<dyn std::error::Error>>(())
388//! ```
389//!
390//! # Keys
391//!
392//! [`Key`] lists every field. Each variant says what it does and what a
393//! process gets without it, and links to the getter of [`Rule`] with the
394//! details: when it applies, which part of Wine reads it and which
395//! built-in rule sets it.
396//!
397//! - Matchers: [`Key::Exe`], [`Key::Company`], [`Key::Product`],
398//! [`Key::OriginalFilename`].
399//! - Scalar settings, each a [`Scalar`] with a [`Setting`]:
400//! [`Key::Dxgi`], [`Key::D3d9`], [`Key::Wined3d`], [`Key::OpenGl`],
401//! [`Key::DpiAware`], [`Key::LargeAddressAware`], [`Key::Gcmouse`],
402//! [`Key::ForceNx`], [`Key::MetalHud`].
403//! - Lists: [`Key::DllOverrides`], [`Key::ArgumentsOverride`],
404//! [`Key::Arguments`], [`Key::DebugChannels`].
405//! - Environment entries: [`Key::Env`].
406//!
407//! [`Key::kind`] says which of these a key is, [`Key::values`] gives the
408//! spellings of a scalar setting, `default` included, [`Key::label`] its
409//! name for a person, and [`Key::upstream`] what upstream Wine does
410//! without it, as `compatdb.so` logs it. For a launcher's templates and
411//! help, [`Key::summary`] says in one line what a key does,
412//! [`Key::example`] gives a value to show and [`Key::docs_url`] links to
413//! its doc; [`DocSection::url`] links to a section of this page.
414//!
415//! # Built-in rules
416//!
417//! [`builtin`] holds the rules compiled into `compatdb.so`, one function
418//! each, whose doc says why the rule exists. [`builtin::defaults`] match
419//! every process: a behaviour of this Wine that differs from upstream Wine
420//! and that a rule can switch is off in the code, and one of these turns
421//! it on, so the log of a process shows its whole configuration with the
422//! source of every value, and a launcher rule that sets the same key
423//! changes it. [`builtin::programs`] each name an executable and pin its
424//! version resource, or name a Steam app id, so none of them catches an
425//! unrelated program of the same name; a test enforces this.
426//!
427//! # Recipes
428//!
429//! Each is one line of [`ENV_VAR`]; `Game.exe` stands for the game's
430//! executable. [`Rule::set`] with the same keys and values builds them.
431//!
432//! | Goal | Rule |
433//! |------|------|
434//! | Re-enable Wine's Mono for every process | `exe=*;dll_overrides=mscoree=b` |
435//! | Prefer a native .NET | `exe=*;dll_overrides=mscoree=n,b` |
436//! | Hand Mono and Gecko back to the registry and `WINEDLLOVERRIDES` | `exe=*;dll_overrides=mscoree,mshtml=default` |
437//! | Make one game DPI-unaware | `exe=Game.exe;dpi_aware=false` |
438//! | Let the registry and the manifest decide DPI awareness | `exe=*;dpi_aware=default` |
439//! | Give one 32-bit game 4 GB | `exe=Game.exe;large_address_aware=true` |
440//! | Give one game the cursor's mouse movement | `exe=Game.exe;gcmouse=false` |
441//! | Give every process wined3d for D3D10 and D3D11, and no D3D12 | `exe=*;dxgi=wined3d` |
442//! | Give every process wined3d for D3D9 | `exe=*;d3d9=wined3d` |
443//! | Hide D3D12 from one game, keeping DXMT for D3D10 and D3D11 | `exe=Game.exe;dll_overrides=d3d12,d3d12core=` |
444//! | Give one game Wine's D3DX9 instead of its native copy | `exe=Game.exe;dll_overrides=d3dx9_43=b` |
445//! | Change a vkd3d-proton setting for one game | `exe=Game.exe;env=VKD3D_CONFIG=<options>` |
446//! | Give one game Wine's no-execute behaviour | `exe=Game.exe;force_nx=false` |
447//! | Give one game FEX's full x87 precision | `exe=Game.exe;env=FEX_X87REDUCEDPRECISION=` |
448//! | Give one game Apple's OpenGL | `exe=Game.exe;opengl=cgl` |
449//! | Put wined3d on Vulkan for one game | `exe=Game.exe;wined3d=vulkan` |
450//! | Show the Metal HUD in one game | `exe=Game.exe;metal_hud=true` |
451//! | Leave one game to upstream Wine for a setting | `exe=Game.exe;<key>=default` |
452//! | Replace the arguments the rules give one game | `exe=Game.exe;arguments_override=-windowed` |
453//! | Give one game no arguments from any rule | `exe=Game.exe;arguments_override=` |
454//! | Change what a built-in rule sets for one program | `exe=Launcher.exe;dxgi=dxmt` |
455//! | Give every program that holds a Steam app id wined3d | `exe=steam:440;dxgi=wined3d` |
456//! | Give one game wined3d by its app id, and not the programs around it | `exe=steam:440;product=<product>;dxgi=wined3d` |
457//!
458//! Most of them with `exe=*` do the same for every process, except where a
459//! built-in rule for one program sets the key. [`Rule::set`] refuses
460//! `arguments_override` in a `*` rule: for every process it would take away
461//! the arguments the built-in rules give single programs.
462//!
463//! Hiding D3D12 disables both of vkd3d-proton's modules, so `d3d12.dll`
464//! fails to load and a game that can use D3D11 falls back to it on DXMT; a
465//! game that only has D3D12 does not start with it, nor with
466//! `dxgi=wined3d`.
467//!
468//! # Launcher responsibilities
469//!
470//! - Write the value with `Table`'s `Display` and nothing else. Never
471//! write the format by hand, add a version header or send the built-in
472//! rules: `Display` leaves them out, since `compatdb.so` has them.
473//! - Remove the variables of [`inherited_conflicts`] from the environment
474//! Wine starts with when the launcher inherited them, so a launch does
475//! not depend on where the launcher was started from.
476//! - Set [`PROF_SERVICE_VAR`] only on purpose, for a launch a profiler
477//! started: every process of the launch sends the service it names its
478//! task control port. An inherited value is one of the variables above.
479//! - Build `WINEDEBUG` itself: Wine reads it before `compatdb.so` loads.
480//! Its own entries come first, then [`Table::winedebug`], joined with
481//! `,`.
482//! - Ask the crate instead of naming keys: [`Key::replaces_on_merge`] when
483//! it lays one layer of its configuration over another, and
484//! [`Key::allowed_on_any`] for the settings it offers for every process.
485//! - Refuse an environment entry for a variable a setting replaced
486//! ([`retired_env`]); [`Rule::set_env`] does, and [`check_env_name`]
487//! checks a name on its own. That includes
488//! `WINEDLLOVERRIDES`: an override `compatdb.so` adds replaces its entry
489//! for the same module, so it cannot undo one, while a rule's
490//! `<module>=default` can.
491//! - Show or hide the Metal HUD with [`Key::MetalHud`], not with its own
492//! `MTL_HUD_` entries in `env`.
493//! - Put `env` entries and `metal_hud` on the rule of the program that
494//! needs them, not on a program that starts games, such as a store
495//! client: the games it starts inherit them.
496//! - Name a former built-in rule with [`builtin::former`], which says what
497//! to do instead; such a name reaches nothing any more.
498//! - Send the rules that hold for the whole launch in [`ENV_VAR`] and point
499//! [`FILE_ENV_VAR`] at a stable file for the rules that change. The file
500//! adds to [`ENV_VAR`] and does not replace it, and [Folding](#folding)
501//! says which rule wins where both set a value.
502//! - When passing rules in a file, set [`FILE_ENV_VAR`] in the environment
503//! the root of the process tree starts with: a program that is already
504//! running does not get a variable set later, and neither do the programs
505//! it starts.
506//! - Write such a file to a temporary file in the same directory and rename
507//! it over the path. A process that reads a file while it is being written
508//! gets a partial table, without a warning.
509//! - Expect a changed table to reach only the processes that start after
510//! the change: a running process keeps the rules it read. So the
511//! processes that outlive one launch, such as `explorer.exe` and
512//! `services.exe`, get it only in the next wineserver session. [`ENV_VAR`]
513//! itself stays fixed for a running tree, since a process inherits the old
514//! value; only the rules of the file change, and a process started later
515//! reads the new ones.
516//! - Expect a change of `debug_channels` in such a file to show only in the
517//! log: `WINEDEBUG` is fixed when the tree starts, so it takes a new tree
518//! for the change to take effect.
519//!
520//! ```
521//! use compatdb_core::{Exe, FieldError, Key, Rule, Table, inherited_conflicts, retired_env};
522//!
523//! let mut game = Rule::new(Exe::new("Game.exe")?);
524//! game.set(Key::DebugChannels, "+relay,-seh")?;
525//! let mut table = Table::new();
526//! table.push(game);
527//! let mut winedebug = vec![String::from("-all")];
528//! winedebug.extend(table.winedebug());
529//! assert_eq!(winedebug.join(","), "-all,Game.exe:+relay,Game.exe:-seh");
530//!
531//! let mut every = Rule::new(Exe::Any);
532//! let error = every.set_env("WINEDLLOVERRIDES", "mscoree=b").err();
533//! assert!(matches!(
534//! error,
535//! Some(FieldError::RetiredEnv {
536//! key: Key::DllOverrides,
537//! ..
538//! })
539//! ));
540//! assert_eq!(retired_env("WineDebug"), Some(Key::DebugChannels));
541//! assert!(inherited_conflicts().contains(&"WINEDLLOVERRIDES"));
542//! # Ok::<(), Box<dyn std::error::Error>>(())
543//! ```
544#![forbid(unsafe_code)]
545#![deny(missing_docs)]
546#![deny(
547 clippy::unwrap_used,
548 clippy::expect_used,
549 clippy::panic,
550 clippy::indexing_slicing,
551 clippy::arithmetic_side_effects
552)]
553
554/// The text of [`DOCS_URL`] as a literal, so `concat!` can build the links
555/// of [`Key::docs_url`] and [`DocSection::url`] from it.
556macro_rules! docs_url {
557 () => {
558 "https://mach9.app/compatdb_core/"
559 };
560}
561
562pub mod builtin;
563mod docs;
564mod error;
565mod key;
566mod part;
567mod resolve;
568mod rule;
569pub mod runtime;
570mod settings;
571mod table;
572mod value;
573mod version;
574mod wire;
575
576pub use crate::{
577 docs::DocSection,
578 error::{Diagnostic, FieldError, ParseError, Severity},
579 key::{Key, KeyKind, Scalar, ScalarValue, ValueKind},
580 part::{
581 Argument, DebugChannel, DllOverride, EnvEntry, EnvName, Exe, ExeName, Fingerprint,
582 Specificity, SteamApp,
583 },
584 resolve::{
585 ArgumentsDropped, DllOverrideReset, EnvFrom, EnvStep, Origin, Resolution, ScalarState,
586 },
587 rule::{DEFAULT_VALUE, Rule, Setting},
588 settings::{
589 check_env_name, inherited_conflicts, retired_env, retired_envs, retired_keys,
590 retired_values,
591 },
592 table::{RulesText, Source, Table},
593 value::{D3d9, Dxgi, METAL_HUD_ENV, OpenGl, Wined3d, metal_hud_env},
594 version::VersionInfo,
595};
596
597/// The environment variable `compatdb.so` reads extra rules from. Wine itself
598/// never looks at it; it is only the channel from whatever started the process
599/// tree to the library.
600pub const ENV_VAR: &str = "WINE_COMPATDB";
601
602/// The environment variable naming a file that holds what [`ENV_VAR`] would
603/// hold, whose rules come after those of [`ENV_VAR`]: see
604/// [Format](index.html#format). Like [`ENV_VAR`], it is only read by
605/// `compatdb.so`.
606pub const FILE_ENV_VAR: &str = "WINE_COMPATDB_FILE";
607
608/// The environment variable naming the bootstrap service of a profiler.
609///
610/// Each process that has it sends that service its task control port, so it
611/// is for a launch a profiler started: see
612/// [Launcher responsibilities](index.html#launcher-responsibilities). It also
613/// gets FEX's debug info: see [`Resolution::fold_profiler_env`].
614pub const PROF_SERVICE_VAR: &str = "WINE_PROF_SERVICE";
615
616/// Where this crate's rustdoc is published. [`Key::docs_url`] and
617/// [`DocSection::url`] link into it.
618pub const DOCS_URL: &str = docs_url!();