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;