compatdb_core/wire.rs
1//! The wire format of [`ENV_VAR`]: one record per rule, its fields
2//! percent-encoded.
3//!
4//! A record is `key=value` fields joined by `;`. In a value, `%`, `;`, CR,
5//! LF and every other control character are written as `%XX`, the hex of
6//! the byte; everything else, non-ASCII included, is written as it is, in
7//! UTF-8. Reading decodes every `%XX` to its byte, and the bytes of a value
8//! must be UTF-8 without a NUL.
9//!
10//! An `env` field is `env=NAME=value`: the name ends at the first `=`, and
11//! an env name cannot contain `=` (see [`EnvName`]). A name that decodes to
12//! one with `=`, from a `%3D`, is refused like any other invalid name.
13
14use std::{
15 fmt::{self, Write as _},
16 str::FromStr,
17};
18
19#[cfg(doc)]
20use crate::ENV_VAR;
21use crate::{
22 Argument, DEFAULT_VALUE, DebugChannel, Diagnostic, DllOverride, EnvEntry, EnvName, Exe,
23 FieldError, Fingerprint, Key, KeyKind, ParseError, Rule, Scalar, Setting, Source, Table,
24};
25
26/// Whether `line` is a version header of the old format, `v=` and nothing
27/// but ASCII digits, which launchers wrote as the first line of a table. The
28/// `v` is a key, so it is matched ASCII-case-insensitively like any other.
29///
30/// [`Table::parse_lenient`] skips such a first line. It can never be a rule:
31/// a rule needs an `exe` field, and this line has only `v`.
32fn is_legacy_header(line: &str) -> bool {
33 line.split_once('=').is_some_and(|(key, digits)| {
34 key.eq_ignore_ascii_case("v")
35 && !digits.is_empty()
36 && digits.bytes().all(|b| b.is_ascii_digit())
37 })
38}
39
40impl Table {
41 /// Read the value of [`ENV_VAR`] the way `compatdb.so` does, which must
42 /// never abort a process: each rule gets its line as its [`Source`], and
43 /// every problem is a [`Diagnostic`] (none when the text is clean).
44 ///
45 /// - A field with an unknown key, a value its key does not take, no
46 /// `=`, escapes that are not UTF-8 or a NUL is left out, and the rest
47 /// of its line still applies.
48 /// - A line without an `exe` is left out as a whole, since it matches no
49 /// process.
50 /// - A second field for a matcher or a scalar setting replaces the first
51 /// one.
52 /// - A `%` that two hex digits do not follow is kept as it is.
53 ///
54 /// A first line that is a version header of the old format
55 /// (`v=<digits>`) is skipped without a diagnostic, so a launcher that
56 /// still writes one keeps working; it still counts as a line.
57 #[must_use]
58 pub fn parse_lenient(text: &str) -> (Self, Vec<Diagnostic>) {
59 let mut lines = text.lines().enumerate().peekable();
60 lines.next_if(|(_, line)| is_legacy_header(line));
61 let mut table = Self::new();
62 let mut diagnostics = Vec::new();
63 for (i, line) in lines {
64 if line.trim().is_empty() {
65 continue;
66 }
67 let n = i.saturating_add(1);
68 match read_record(line, n, &mut diagnostics) {
69 Some(rule) => table.push_line(rule, n),
70 None => diagnostics.push(Diagnostic {
71 line: n,
72 field: None,
73 error: FieldError::MissingExe,
74 }),
75 }
76 }
77 (table, diagnostics)
78 }
79
80 /// Read the value of [`ENV_VAR`], refusing any problem that
81 /// [`Table::parse_lenient`] would report. A table this gives back writes
82 /// itself (see [`Table`]'s `Display`) as `text` with blank lines and a
83 /// legacy header dropped and every word in lowercase.
84 ///
85 /// # Errors
86 ///
87 /// A [`ParseError`] with every diagnostic, when there is one.
88 pub fn parse_strict(text: &str) -> Result<Self, ParseError> {
89 let (table, diagnostics) = Self::parse_lenient(text);
90 if diagnostics.is_empty() {
91 Ok(table)
92 } else {
93 Err(ParseError(diagnostics))
94 }
95 }
96}
97
98impl FromStr for Table {
99 type Err = ParseError;
100
101 /// [`Table::parse_strict`].
102 fn from_str(s: &str) -> Result<Self, Self::Err> {
103 Self::parse_strict(s)
104 }
105}
106
107impl fmt::Display for Table {
108 /// The value of [`ENV_VAR`]: one record per rule, one per line, for
109 /// every rule that is not built in. The built-in rules are left out:
110 /// `compatdb.so` has them compiled in, and the same rules again from the
111 /// variable would fold after all of them.
112 ///
113 /// [`Table::parse_strict`] reads a table this writes back as the same
114 /// table when the table's own text is canonical: the table of
115 /// [`Table::new`] and [`Table::push`], or of [`Table::parse_strict`] from
116 /// text this wrote.
117 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118 let sent = self
119 .entries()
120 .filter(|(source, _)| *source != Source::BuiltIn)
121 .map(|(_, rule)| rule);
122 for (i, rule) in sent.enumerate() {
123 if i > 0 {
124 f.write_str("\n")?;
125 }
126 f.write_str(&rule_to_record(rule))?;
127 }
128 Ok(())
129 }
130}
131
132/// Serialize one rule.
133///
134/// A library that does not know a key added later ignores that field with
135/// a diagnostic and applies the rest of the record. That is safe only for a
136/// setting, so the matcher keys (`exe`, `company`, `product`,
137/// `original_filename`) are a closed set and a new key is only ever a
138/// setting: a library that ignored an unknown matcher would apply the rule
139/// to more processes than it names.
140fn rule_to_record(rule: &Rule) -> String {
141 let mut fields: Vec<String> = vec![field(Key::Exe, rule.exe.as_str())];
142 let pins = [
143 (Key::Company, &rule.company),
144 (Key::Product, &rule.product),
145 (Key::OriginalFilename, &rule.original_filename),
146 ];
147 for (key, pin) in pins {
148 if let Some(pin) = pin {
149 fields.push(field(key, pin.as_str()));
150 }
151 }
152 for &scalar in Scalar::ALL {
153 match rule.scalar(scalar) {
154 Setting::Unset => {}
155 Setting::Set(value) => fields.push(field(scalar.key(), value.as_str())),
156 Setting::Reset => fields.push(field(scalar.key(), DEFAULT_VALUE)),
157 }
158 }
159 for over in &rule.dll_overrides {
160 fields.push(field(Key::DllOverrides, over.as_str()));
161 }
162 match &rule.arguments_override {
163 // An override without entries clears the list, so it travels as the
164 // key with an empty value.
165 Some(texts) if texts.is_empty() => fields.push(field(Key::ArgumentsOverride, "")),
166 Some(texts) => fields.extend(
167 texts
168 .iter()
169 .map(|t| field(Key::ArgumentsOverride, t.as_str())),
170 ),
171 None => {}
172 }
173 for text in &rule.arguments {
174 fields.push(field(Key::Arguments, text.as_str()));
175 }
176 for channel in &rule.debug_channels {
177 fields.push(field(Key::DebugChannels, channel.as_str()));
178 }
179 for entry in &rule.env {
180 // The name has no `=`, so the first `=` of the value ends it.
181 let mut value = String::new();
182 encode_into(entry.name().as_str(), &mut value);
183 value.push('=');
184 encode_into(entry.value().unwrap_or_default(), &mut value);
185 fields.push(format!("{}={value}", Key::Env));
186 }
187 fields.join(";")
188}
189
190/// Read one record into a rule, or `None` for a record without an `exe`,
191/// which matches no process. Every problem with a field goes to
192/// `diagnostics`; a field that cannot be read is left out, and the other
193/// fields still apply. `line` is the record's line, for the diagnostics.
194///
195/// The matcher keys are a closed set (see [`rule_to_record`]), so an unknown
196/// key is a setting this library does not know, or a typo. A misspelt
197/// matcher is ignored like any other unknown key, with the warning, and the
198/// rule then matches more processes than it meant to. The `name` and
199/// `enabled` keys of the old format, where an override merged into or
200/// dropped a rule by name, are unknown keys too.
201fn read_record(record: &str, line: usize, diagnostics: &mut Vec<Diagnostic>) -> Option<Rule> {
202 let mut rule = Rule::new(Exe::Any);
203 // The rule's `exe`, which every rule needs; an empty one is none.
204 let mut exe = None;
205 // The keys that hold one value and a field of the record has set.
206 let mut seen: Vec<Key> = Vec::new();
207 for raw in record.split(';') {
208 if raw.is_empty() {
209 continue;
210 }
211 let mut report = |error| {
212 diagnostics.push(Diagnostic {
213 line,
214 field: Some(raw.to_owned()),
215 error,
216 });
217 };
218 let Some((key, value)) = raw.split_once('=') else {
219 report(FieldError::MissingEquals);
220 continue;
221 };
222 let key: Key = match key.parse() {
223 Ok(key) => key,
224 Err(error) => {
225 report(error);
226 continue;
227 }
228 };
229 // One diagnostic per field: why it was left out, or else that it
230 // replaced an earlier one, or else that it kept a `%` as written.
231 let mut decoder = Decoder::default();
232 if let Err(error) = read_field(&mut rule, &mut exe, key, value, &mut decoder) {
233 report(error);
234 continue;
235 }
236 let holds_one = matches!(key.kind(), KeyKind::Matcher | KeyKind::Scalar(_));
237 if holds_one && seen.contains(&key) {
238 report(FieldError::Duplicate(key));
239 } else if decoder.bad_escape {
240 report(FieldError::BadEscape);
241 }
242 if holds_one && !seen.contains(&key) {
243 seen.push(key);
244 }
245 }
246 exe.map(|exe| {
247 rule.exe = exe;
248 rule
249 })
250}
251
252/// Set the field `key` of `rule`, or of `exe`, from its wire `value`, or
253/// say why it cannot be set, leaving both as they were.
254fn read_field(
255 rule: &mut Rule,
256 exe: &mut Option<Exe>,
257 key: Key,
258 value: &str,
259 decoder: &mut Decoder,
260) -> Result<(), FieldError> {
261 if let Some(scalar) = key.scalar() {
262 let text = decoder.decode(value)?;
263 // `default`, in any case, resets the setting (see
264 // [`Setting::Reset`]); anything else is read as a value of it.
265 let setting = if text.eq_ignore_ascii_case(DEFAULT_VALUE) {
266 Setting::Reset
267 } else {
268 Setting::Set(scalar.parse_value(&text)?)
269 };
270 rule.put_scalar(scalar, setting);
271 return Ok(());
272 }
273 match key {
274 Key::Exe => {
275 *exe = match Exe::new(decoder.decode(value)?) {
276 Ok(name) => Some(name),
277 Err(FieldError::MissingExe) => None,
278 Err(error) => return Err(error),
279 };
280 }
281 Key::Company => rule.company = Some(Fingerprint::new(decoder.decode(value)?)?),
282 Key::Product => rule.product = Some(Fingerprint::new(decoder.decode(value)?)?),
283 Key::OriginalFilename => {
284 rule.original_filename = Some(Fingerprint::new(decoder.decode(value)?)?);
285 }
286 Key::DllOverrides => {
287 let entry = DllOverride::new(decoder.decode(value)?)?;
288 rule.dll_overrides.push(entry);
289 }
290 Key::Arguments => rule.arguments.push(Argument::new(decoder.decode(value)?)?),
291 Key::DebugChannels => {
292 let channel = DebugChannel::new(decoder.decode(value)?)?;
293 rule.debug_channels.push(channel);
294 }
295 // Present at all, the override is set; an empty value adds no entry,
296 // so `arguments_override=` alone clears the list.
297 Key::ArgumentsOverride => {
298 let argument = if value.is_empty() {
299 None
300 } else {
301 Some(Argument::new(decoder.decode(value)?)?)
302 };
303 rule.arguments_override
304 .get_or_insert_default()
305 .extend(argument);
306 }
307 // The name ends at the first `=`; a name cannot contain one, so a
308 // `%3D` in it is refused. A value without `=` is the variable alone,
309 // which it removes.
310 Key::Env => {
311 let (name, val) = value.split_once('=').unwrap_or((value, ""));
312 let name = EnvName::new(decoder.decode(name)?)?;
313 rule.env.push(EnvEntry::new(name, decoder.decode(val)?)?);
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/// The field `key=value`, with the value escaped.
330fn field(key: Key, value: &str) -> String {
331 let mut out = format!("{key}=");
332 encode_into(value, &mut out);
333 out
334}
335
336/// Percent-escape the characters that would collide with the wire format
337/// (`%`, `;`, and CR/LF) plus any other control character. Everything else,
338/// including `=`, `,`, spaces and non-ASCII, passes through.
339fn encode_into(raw: &str, out: &mut String) {
340 for c in raw.chars() {
341 match c {
342 '%' => out.push_str("%25"),
343 ';' => out.push_str("%3B"),
344 c if c.is_ascii_control() => {
345 let _ = write!(out, "%{:02X}", u32::from(c));
346 }
347 c => out.push(c),
348 }
349 }
350}
351
352/// The decoder of the values of one field, which notes a `%` it kept
353/// because two hex digits did not follow it.
354#[derive(Default)]
355struct Decoder {
356 /// Whether a value had such a `%`.
357 bad_escape: bool,
358}
359
360impl Decoder {
361 /// Reverse [`encode_into`]: every `%XX` becomes the byte `XX`, in either
362 /// case, and the bytes must be UTF-8 without a NUL. A `%` that two hex
363 /// digits do not follow stays as it is.
364 fn decode(&mut self, encoded: &str) -> Result<String, FieldError> {
365 let mut bytes = Vec::with_capacity(encoded.len());
366 let mut rest = encoded.as_bytes();
367 while let Some((&byte, tail)) = rest.split_first() {
368 rest = tail;
369 if byte != b'%' {
370 bytes.push(byte);
371 continue;
372 }
373 if let Some(escaped) = hex_pair(rest) {
374 bytes.push(escaped);
375 rest = rest.get(2..).unwrap_or_default();
376 } else {
377 self.bad_escape = true;
378 bytes.push(byte);
379 }
380 }
381 let text = String::from_utf8(bytes).map_err(|_| FieldError::InvalidUtf8)?;
382 if text.contains('\0') {
383 return Err(FieldError::Nul);
384 }
385 Ok(text)
386 }
387}
388
389/// The byte that the two hex digits at the start of `bytes` spell, if they
390/// are two hex digits.
391fn hex_pair(bytes: &[u8]) -> Option<u8> {
392 let pair = bytes.get(..2)?;
393 if !pair.iter().all(u8::is_ascii_hexdigit) {
394 return None;
395 }
396 u8::from_str_radix(std::str::from_utf8(pair).ok()?, 16).ok()
397}
398
399#[cfg(test)]
400mod tests;