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
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;