compatdb_core/table.rs
1//! The table of rules, each with where it comes from.
2
3#[cfg(doc)]
4use crate::Origin;
5use crate::{ENV_VAR, Exe, FILE_ENV_VAR, Resolution, Rule, Specificity, VersionInfo, builtin};
6
7/// Where a rule of a [`Table`] comes from. It decides how the log names the
8/// rule's values (see [`Origin`]) and whether its `env` entries are defaults
9/// (see [`Origin::env_is_default`]). Never serialized.
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum Source {
12 /// Compiled into `compatdb.so`: a rule of [`Table::builtin`].
13 BuiltIn,
14 /// Line `n` of [`ENV_VAR`], counted from 1 as [`Table::parse_lenient`]
15 /// reads the value, blank lines and a legacy header included.
16 Line(usize),
17 /// Line `n` of the file [`FILE_ENV_VAR`] names, counted from 1 as
18 /// [`Table::parse_file_lenient`] reads the file. The file counts its own
19 /// lines, so a rule here can share its number with a [`Source::Line`].
20 FileLine(usize),
21}
22
23/// Which of the two texts of the caller's rules a line is in. Each counts its
24/// own lines, so a line number alone does not say where a problem is.
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum RulesText {
27 /// The value of [`ENV_VAR`].
28 Variable,
29 /// The file [`FILE_ENV_VAR`] names.
30 File,
31}
32
33impl RulesText {
34 /// How the log names the text: by its variable.
35 pub(crate) const fn name(self) -> &'static str {
36 match self {
37 Self::Variable => ENV_VAR,
38 Self::File => FILE_ENV_VAR,
39 }
40 }
41
42 /// The source of a rule on line `line` of the text.
43 pub(crate) const fn source(self, line: usize) -> Source {
44 match self {
45 Self::Variable => Source::Line(line),
46 Self::File => Source::FileLine(line),
47 }
48 }
49}
50
51/// The whole database: the built-in rules followed by the rules of
52/// [`ENV_VAR`] and then those of the file [`FILE_ENV_VAR`] names, in the
53/// order they were declared, each with its [`Source`].
54///
55/// A launcher builds the rules it sends with [`Table::new`] and
56/// [`Table::push`] and writes them with `to_string()`, which is the value of
57/// [`ENV_VAR`]; [`Table::with_builtin`] puts the built-in rules in front, as
58/// `compatdb.so` does, to see what a process would get.
59#[derive(Clone, Default, Debug, PartialEq, Eq)]
60pub struct Table {
61 /// The rules, in the order they were declared, which is the order
62 /// [`Table::resolve`] folds rules of equal place in its order in.
63 pub(crate) rules: Vec<Rule>,
64 /// Where each rule comes from, at the same index.
65 pub(crate) sources: Vec<Source>,
66}
67
68impl Table {
69 /// The table without rules.
70 #[must_use]
71 pub const fn new() -> Self {
72 Self {
73 rules: Vec::new(),
74 sources: Vec::new(),
75 }
76 }
77
78 /// The table of the built-in rules ([`builtin::rules`]), which
79 /// `compatdb.so` puts in front of the rules of [`ENV_VAR`].
80 #[must_use]
81 pub fn builtin() -> Self {
82 Self::of_builtin(builtin::rules())
83 }
84
85 /// The table of `rules`, each a built-in rule.
86 pub(crate) fn of_builtin(rules: Vec<Rule>) -> Self {
87 Self {
88 sources: vec![Source::BuiltIn; rules.len()],
89 rules,
90 }
91 }
92
93 /// This table with the built-in rules in front of its own, as
94 /// `compatdb.so` builds its table.
95 #[must_use]
96 pub fn with_builtin(self) -> Self {
97 let mut table = Self::builtin();
98 table.append(self);
99 table
100 }
101
102 /// Add the rules of `other` after these, with their sources, as
103 /// `compatdb.so` adds the rules of the file [`FILE_ENV_VAR`] names after
104 /// those of [`ENV_VAR`].
105 ///
106 /// A table that holds a file's rules is for [`Table::resolve`] and
107 /// [`Table::preview`]. `Display` writes those rules too, so its text is
108 /// no value for [`ENV_VAR`] beside the file: they would fold twice.
109 pub fn append(&mut self, other: Self) {
110 self.rules.extend(other.rules);
111 self.sources.extend(other.sources);
112 }
113
114 /// Add `rule` after the others, with the source `source`.
115 pub(crate) fn push_from(&mut self, rule: Rule, source: Source) {
116 self.rules.push(rule);
117 self.sources.push(source);
118 }
119
120 /// Add `rule` after the others, as a rule of [`ENV_VAR`] on the line
121 /// after the last line of that variable in the table, which for a table
122 /// built with this method alone is its line in the value the table
123 /// writes.
124 pub fn push(&mut self, rule: Rule) {
125 let last = self
126 .sources
127 .iter()
128 .filter_map(|source| match source {
129 Source::Line(n) => Some(*n),
130 Source::BuiltIn | Source::FileLine(_) => None,
131 })
132 .max()
133 .unwrap_or(0);
134 self.push_from(rule, Source::Line(last.saturating_add(1)));
135 }
136
137 /// The rules, in the order they were declared.
138 #[must_use]
139 pub fn rules(&self) -> &[Rule] {
140 &self.rules
141 }
142
143 /// Where each rule of [`Table::rules`] comes from, at the same index.
144 #[must_use]
145 pub fn sources(&self) -> &[Source] {
146 &self.sources
147 }
148
149 /// Each rule with its source, in the order they were declared.
150 pub(crate) fn entries(&self) -> impl Iterator<Item = (Source, &Rule)> {
151 self.sources.iter().copied().zip(&self.rules)
152 }
153
154 /// What a process with the image basename `exe` and the version
155 /// resource `version` would get from `compatdb.so` with this table in
156 /// [`ENV_VAR`]: the built-in rules followed by this table's own (a
157 /// built-in rule in this table is left out, since the library has it
158 /// already), resolved, with [`Resolution::fold_derived_env`] done.
159 ///
160 /// Which env entries apply also depends on the environment the process
161 /// starts with, which [`Resolution::env_steps`] takes.
162 ///
163 /// The process has no Steam app id here, so a rule for one
164 /// ([`Exe::Steam`]) does not apply.
165 #[must_use]
166 pub fn preview(&self, exe: &str, version: &VersionInfo) -> Resolution {
167 let mut table = Self::builtin();
168 for (source, rule) in self.entries() {
169 if source != Source::BuiltIn {
170 table.rules.push(rule.clone());
171 table.sources.push(source);
172 }
173 }
174 let mut resolution = table.resolve(exe, version, None);
175 resolution.fold_derived_env();
176 resolution
177 }
178
179 /// The `WINEDEBUG` entries the `debug_channels` of this table's rules
180 /// stand for, in table order. A launcher joins them with `,` after its
181 /// own entries and starts Wine with the result, since Wine reads the
182 /// variable before `compatdb.so` loads.
183 ///
184 /// An entry of a rule for an executable is scoped to it as
185 /// `<exe>:<entry>`, which is Wine's own syntax: Wine compares the prefix
186 /// with the image file name ASCII-case-insensitively and knows nothing
187 /// of fingerprints, so the entry reaches every program of that name. An
188 /// entry of a rule with `exe` `*` is unscoped. Wine lets a later entry
189 /// win for the same channel.
190 ///
191 /// An entry of a rule for a Steam app id is left out: Wine has no scope
192 /// for an app id, and without one the entry would reach every process.
193 /// [`Rule::set`] refuses such an entry.
194 #[must_use]
195 pub fn winedebug(&self) -> Vec<String> {
196 self.rules
197 .iter()
198 .flat_map(|rule| {
199 rule.debug_channels
200 .iter()
201 .filter_map(move |channel| match &rule.exe {
202 Exe::Any => Some(channel.to_string()),
203 Exe::Name(name) => Some(format!("{name}:{channel}")),
204 Exe::Steam(_) => None,
205 })
206 })
207 .collect()
208 }
209
210 /// Every pair of rules that matches exactly the same processes, by their
211 /// index in the table. A duplicate can only be a mistake, so the
212 /// launcher warns about it.
213 ///
214 /// Plain wildcard rules (`exe = *` with no fingerprint) are left out: they
215 /// are defaults that are meant to stack, one per setting, so several of
216 /// them matching every process is not a mistake.
217 #[must_use]
218 pub fn duplicate_matchers(&self) -> Vec<(usize, usize)> {
219 let mut pairs = Vec::new();
220 for (i, rule) in self.rules.iter().enumerate() {
221 if rule.specificity() == Specificity::Any {
222 continue;
223 }
224 for (j, other) in self.rules.iter().enumerate().skip(i.saturating_add(1)) {
225 if rule.matches_the_same_as(other) {
226 pairs.push((i, j));
227 }
228 }
229 }
230 pairs
231 }
232}
233
234#[cfg(test)]
235mod tests;