Skip to main content

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