Skip to main content

compatdb_core/
part.rs

1//! The typed parts of a rule: its `exe` and fingerprint values, and the
2//! entries of its lists. Each part checks its text when it is made, so every
3//! rule that can be built can also be written to [`ENV_VAR`] and read back.
4//!
5//! No part holds a NUL: the process APIs that take these texts end a string
6//! at the first one.
7
8use std::{fmt, str::FromStr};
9
10use crate::{DEFAULT_VALUE, FieldError};
11#[cfg(doc)]
12use crate::{ENV_VAR, Rule};
13
14/// `text` itself, or [`FieldError::Nul`] when it holds a NUL.
15fn without_nul(text: String) -> Result<String, FieldError> {
16    if text.contains('\0') {
17        Err(FieldError::Nul)
18    } else {
19        Ok(text)
20    }
21}
22
23/// The word in front of the app id of an [`Exe::Steam`], as the wire writes
24/// it. Like every word of the format it is read ASCII-case-insensitively.
25const STEAM_PREFIX: &str = "steam:";
26
27/// What follows a `steam:` in any case at the start of `text`, if one is
28/// there.
29fn after_steam_prefix(text: &str) -> Option<&str> {
30    let (word, rest) = text.split_at_checked(STEAM_PREFIX.len())?;
31    word.eq_ignore_ascii_case(STEAM_PREFIX).then_some(rest)
32}
33
34/// The Steam app id `digits` spell: ASCII digits only, which make a `u32`
35/// other than 0. 0 is no app.
36pub fn steam_app_id(digits: &str) -> Option<u32> {
37    if !digits.bytes().all(|byte| byte.is_ascii_digit()) {
38        return None;
39    }
40    digits.parse().ok().filter(|&id| id != 0)
41}
42
43/// The executable a rule matches: every process, the processes whose image
44/// basename is a name, compared ASCII-case-insensitively, or the processes
45/// with a Steam app id.
46#[derive(Clone, Debug, PartialEq, Eq, Hash)]
47pub enum Exe {
48    /// `*`: every process.
49    Any,
50    /// The processes with this image basename.
51    Name(ExeName),
52    /// `steam:<appid>`: the processes with this Steam app id, whatever their
53    /// image basename.
54    Steam(SteamApp),
55}
56
57/// The image basename of an [`Exe::Name`]: not empty, not `*`, not a text
58/// that starts with `steam:` in any case and without a NUL. [`Exe::new`] makes
59/// one.
60#[derive(Clone, Debug, PartialEq, Eq, Hash)]
61pub struct ExeName(String);
62
63/// The Steam app of an [`Exe::Steam`]: an id other than 0, with the text the
64/// wire writes for it. [`Exe::new`] makes one.
65#[derive(Clone, Debug, PartialEq, Eq, Hash)]
66pub struct SteamApp {
67    /// The app id.
68    id: u32,
69    /// `steam:<id>`, in lowercase and without leading zeros.
70    text: String,
71}
72
73impl Exe {
74    /// The spelling of [`Exe::Any`].
75    pub const ANY: &'static str = "*";
76
77    /// The `exe` that `text` names: `*` is [`Exe::Any`], `steam:` in any case
78    /// and a decimal app id [`Exe::Steam`], anything else the basename as
79    /// written. A basename cannot hold a `:`, so no program is left out.
80    ///
81    /// # Errors
82    ///
83    /// [`FieldError::MissingExe`] for an empty `text`, [`FieldError::Nul`]
84    /// for one with a NUL, [`FieldError::BadSteamApp`] for a `steam:` that
85    /// no app id follows: nothing, anything but ASCII digits, 0 or a value
86    /// over [`u32::MAX`].
87    pub fn new(text: impl Into<String>) -> Result<Self, FieldError> {
88        let text = without_nul(text.into())?;
89        if let Some(digits) = after_steam_prefix(&text) {
90            let app = steam_app_id(digits).map(SteamApp::of_id);
91            return app.map(Self::Steam).ok_or(FieldError::BadSteamApp(text));
92        }
93        match text.as_str() {
94            "" => Err(FieldError::MissingExe),
95            Self::ANY => Ok(Self::Any),
96            _ => Ok(Self::Name(ExeName(text))),
97        }
98    }
99
100    /// `text` as an `exe`, unchecked: only for the literals of this crate,
101    /// which a test reads back. `steam:<appid>` is [`Exe::Steam`], as for
102    /// [`Exe::new`].
103    pub(crate) fn literal(text: &str) -> Self {
104        if text == Self::ANY {
105            return Self::Any;
106        }
107        after_steam_prefix(text).and_then(steam_app_id).map_or_else(
108            || Self::Name(ExeName(text.to_owned())),
109            |id| Self::Steam(SteamApp::of_id(id)),
110        )
111    }
112
113    /// `*`, the basename or `steam:<appid>`, as the wire writes it.
114    #[must_use]
115    pub fn as_str(&self) -> &str {
116        match self {
117            Self::Any => Self::ANY,
118            Self::Name(name) => name.as_str(),
119            Self::Steam(app) => app.as_str(),
120        }
121    }
122
123    /// Whether this is [`Exe::Any`].
124    #[must_use]
125    pub const fn is_any(&self) -> bool {
126        matches!(self, Self::Any)
127    }
128
129    /// Whether this is [`Exe::Steam`].
130    #[must_use]
131    pub const fn is_steam_app(&self) -> bool {
132        matches!(self, Self::Steam(_))
133    }
134
135    /// Whether a process with the image basename `basename` and the Steam
136    /// app id `steam_app`, if it has one, matches.
137    #[must_use]
138    pub fn matches(&self, basename: &str, steam_app: Option<u32>) -> bool {
139        match self {
140            Self::Any => true,
141            Self::Name(name) => basename.eq_ignore_ascii_case(name.as_str()),
142            Self::Steam(app) => steam_app == Some(app.id()),
143        }
144    }
145}
146
147impl ExeName {
148    /// The basename as written.
149    #[must_use]
150    pub fn as_str(&self) -> &str {
151        &self.0
152    }
153}
154
155impl SteamApp {
156    /// The app with the id `id`, which is not 0.
157    fn of_id(id: u32) -> Self {
158        Self {
159            id,
160            text: format!("{STEAM_PREFIX}{id}"),
161        }
162    }
163
164    /// The app id.
165    #[must_use]
166    pub const fn id(&self) -> u32 {
167        self.id
168    }
169
170    /// `steam:<id>`, as the wire writes it.
171    #[must_use]
172    pub fn as_str(&self) -> &str {
173        &self.text
174    }
175}
176
177impl FromStr for Exe {
178    type Err = FieldError;
179
180    /// See [`Exe::new`].
181    fn from_str(s: &str) -> Result<Self, Self::Err> {
182        Self::new(s)
183    }
184}
185
186impl fmt::Display for Exe {
187    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188        f.write_str(self.as_str())
189    }
190}
191
192impl fmt::Display for ExeName {
193    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194        f.write_str(self.as_str())
195    }
196}
197
198impl fmt::Display for SteamApp {
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        f.write_str(self.as_str())
201    }
202}
203
204impl PartialEq<&str> for Exe {
205    fn eq(&self, other: &&str) -> bool {
206        self.as_str() == *other
207    }
208}
209
210/// How specific a rule's matchers are, least specific first (see
211/// [`Table::resolve`](crate::Table::resolve) for how it orders the rules
212/// with their source).
213#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
214pub enum Specificity {
215    /// `exe` `*` and no fingerprint: a default for every process.
216    Any,
217    /// `exe` `*` with a fingerprint.
218    AnyWithFingerprint,
219    /// An executable name or a Steam app id without a fingerprint.
220    Exe,
221    /// An executable name or a Steam app id with a fingerprint.
222    ExeWithFingerprint,
223}
224
225/// A fingerprint value: a substring that one string of the image's version
226/// resource must contain, compared ASCII-case-insensitively. Any text
227/// without a NUL; an empty one matches every image.
228#[derive(Clone, Debug, PartialEq, Eq, Hash)]
229pub struct Fingerprint(String);
230
231impl Fingerprint {
232    /// `text` as a fingerprint value.
233    ///
234    /// # Errors
235    ///
236    /// [`FieldError::Nul`] when `text` holds a NUL.
237    pub fn new(text: impl Into<String>) -> Result<Self, FieldError> {
238        without_nul(text.into()).map(Self)
239    }
240
241    /// `text` as a fingerprint value, unchecked: only for the literals of
242    /// this crate, which a test reads back.
243    pub(crate) fn literal(text: &str) -> Self {
244        Self(text.to_owned())
245    }
246
247    /// The value as written.
248    #[must_use]
249    pub fn as_str(&self) -> &str {
250        &self.0
251    }
252
253    /// Whether the version string `actual` contains this value, compared
254    /// ASCII-case-insensitively.
255    #[must_use]
256    pub fn matches(&self, actual: &str) -> bool {
257        actual
258            .to_ascii_lowercase()
259            .contains(&self.0.to_ascii_lowercase())
260    }
261}
262
263/// Text appended to the command line: not empty and without a NUL.
264#[derive(Clone, Debug, PartialEq, Eq, Hash)]
265pub struct Argument(String);
266
267impl Argument {
268    /// `text` as an argument.
269    ///
270    /// # Errors
271    ///
272    /// [`FieldError::EmptyArgument`] for an empty `text`,
273    /// [`FieldError::Nul`] for one with a NUL.
274    pub fn new(text: impl Into<String>) -> Result<Self, FieldError> {
275        let text = without_nul(text.into())?;
276        if text.is_empty() {
277            Err(FieldError::EmptyArgument)
278        } else {
279            Ok(Self(text))
280        }
281    }
282
283    /// `text` as an argument, unchecked: only for the literals of this
284    /// crate, which a test reads back.
285    pub(crate) fn literal(text: &str) -> Self {
286        Self(text.to_owned())
287    }
288
289    /// The text as written.
290    #[must_use]
291    pub fn as_str(&self) -> &str {
292        &self.0
293    }
294}
295
296/// One `WINEDEBUG` entry of a rule, such as `+relay` or `warn-heap`: not
297/// empty, without `,` and without a NUL. The text is kept as written; Wine
298/// reads it.
299///
300/// A `,` separates the entries of `WINEDEBUG`, so the text after one would
301/// be an entry of its own, which the `<exe>:` scope of
302/// [`Table::winedebug`](crate::Table::winedebug) does not reach.
303#[derive(Clone, Debug, PartialEq, Eq, Hash)]
304pub struct DebugChannel(String);
305
306impl DebugChannel {
307    /// `text` as a `WINEDEBUG` entry.
308    ///
309    /// # Errors
310    ///
311    /// [`FieldError::EmptyDebugChannel`] for an empty `text`,
312    /// [`FieldError::CommaInDebugChannel`] for one with a `,`,
313    /// [`FieldError::Nul`] for one with a NUL.
314    pub fn new(text: impl Into<String>) -> Result<Self, FieldError> {
315        let text = without_nul(text.into())?;
316        if text.is_empty() {
317            Err(FieldError::EmptyDebugChannel)
318        } else if text.contains(',') {
319            Err(FieldError::CommaInDebugChannel(text))
320        } else {
321            Ok(Self(text))
322        }
323    }
324
325    /// The entry as written.
326    #[must_use]
327    pub fn as_str(&self) -> &str {
328        &self.0
329    }
330}
331
332/// One `WINEDLLOVERRIDES` element, `<modules>=<order>`.
333///
334/// That is comma-separated module names, `=`, and a load order such as
335/// `n,b`, empty to disable the modules, or `default` to take them out of the
336/// overrides of the rules folded before. The text is kept as written.
337#[derive(Clone, Debug, PartialEq, Eq, Hash)]
338pub struct DllOverride(String);
339
340impl DllOverride {
341    /// `text` as a DLL override.
342    ///
343    /// # Errors
344    ///
345    /// [`FieldError::InvalidDllOverride`] when `text` has no `=`,
346    /// [`FieldError::Nul`] when it holds a NUL.
347    pub fn new(text: impl Into<String>) -> Result<Self, FieldError> {
348        let text = without_nul(text.into())?;
349        if text.contains('=') {
350            Ok(Self(text))
351        } else {
352            Err(FieldError::InvalidDllOverride(text))
353        }
354    }
355
356    /// `text` as a DLL override, unchecked: only for the literals of this
357    /// crate, which a test reads back, and for overrides made from the parts
358    /// of a checked one.
359    pub(crate) fn literal(text: &str) -> Self {
360        Self(text.to_owned())
361    }
362
363    /// The text as written, which is also its form in `WINEDLLOVERRIDES`.
364    #[must_use]
365    pub fn as_str(&self) -> &str {
366        &self.0
367    }
368
369    /// The module names before the `=`, trimmed, without empty ones.
370    pub fn modules(&self) -> impl Iterator<Item = &str> {
371        self.names()
372            .split(',')
373            .map(str::trim)
374            .filter(|name| !name.is_empty())
375    }
376
377    /// The load order after the first `=`, as written.
378    #[must_use]
379    pub fn order(&self) -> &str {
380        self.0.split_once('=').map_or("", |(_, order)| order)
381    }
382
383    /// Whether the order is `default`, in any case and with surrounding
384    /// spaces: such an entry sets no load order, it takes its modules out of
385    /// the entries before it.
386    #[must_use]
387    pub fn is_default(&self) -> bool {
388        self.order().trim().eq_ignore_ascii_case(DEFAULT_VALUE)
389    }
390
391    /// The text before the first `=`.
392    fn names(&self) -> &str {
393        self.0.split_once('=').map_or("", |(names, _)| names)
394    }
395}
396
397/// The name of an environment variable: not empty, without `=` and without a
398/// NUL. It is kept as written; a process environment matches it
399/// ASCII-case-insensitively.
400#[derive(Clone, Debug, PartialEq, Eq, Hash)]
401pub struct EnvName(String);
402
403impl EnvName {
404    /// `text` as a variable name.
405    ///
406    /// # Errors
407    ///
408    /// [`FieldError::InvalidEnvName`] when `text` is empty or holds a `=`,
409    /// [`FieldError::Nul`] when it holds a NUL.
410    pub fn new(text: impl Into<String>) -> Result<Self, FieldError> {
411        let text = without_nul(text.into())?;
412        if text.is_empty() || text.contains('=') {
413            Err(FieldError::InvalidEnvName(text))
414        } else {
415            Ok(Self(text))
416        }
417    }
418
419    /// The name as written.
420    #[must_use]
421    pub fn as_str(&self) -> &str {
422        &self.0
423    }
424}
425
426/// One environment entry of a rule: a variable and the value it gets, or no
427/// value, which removes the variable.
428#[derive(Clone, Debug, PartialEq, Eq, Hash)]
429pub struct EnvEntry {
430    /// The variable.
431    name: EnvName,
432    /// Its value, not empty, or `None` to remove it.
433    value: Option<String>,
434}
435
436impl EnvEntry {
437    /// The entry that sets `name` to `value`. An empty `value` removes the
438    /// variable, as on the wire, where an empty value is a removal.
439    ///
440    /// # Errors
441    ///
442    /// [`FieldError::Nul`] when `value` holds a NUL.
443    pub fn new(name: EnvName, value: impl Into<String>) -> Result<Self, FieldError> {
444        let value = without_nul(value.into())?;
445        Ok(Self {
446            name,
447            value: (!value.is_empty()).then_some(value),
448        })
449    }
450
451    /// The entry that removes `name`.
452    #[must_use]
453    pub const fn remove(name: EnvName) -> Self {
454        Self { name, value: None }
455    }
456
457    /// The entry that sets `name` to `value`, unchecked: only for the
458    /// literals of this crate, which a test reads back, and for values made
459    /// from checked ones.
460    pub(crate) fn literal(name: &str, value: &str) -> Self {
461        Self {
462            name: EnvName(name.to_owned()),
463            value: (!value.is_empty()).then(|| value.to_owned()),
464        }
465    }
466
467    /// The variable.
468    #[must_use]
469    pub const fn name(&self) -> &EnvName {
470        &self.name
471    }
472
473    /// The value it gets, or `None` when the entry removes it.
474    #[must_use]
475    pub fn value(&self) -> Option<&str> {
476        self.value.as_deref()
477    }
478}
479
480impl fmt::Display for EnvEntry {
481    /// `NAME=value`, or `NAME=` for a removal.
482    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
483        write!(f, "{}={}", self.name, self.value().unwrap_or_default())
484    }
485}
486
487impl FromStr for Fingerprint {
488    type Err = FieldError;
489
490    /// See [`Fingerprint::new`].
491    fn from_str(s: &str) -> Result<Self, Self::Err> {
492        Self::new(s)
493    }
494}
495
496impl FromStr for Argument {
497    type Err = FieldError;
498
499    /// See [`Argument::new`].
500    fn from_str(s: &str) -> Result<Self, Self::Err> {
501        Self::new(s)
502    }
503}
504
505impl FromStr for DebugChannel {
506    type Err = FieldError;
507
508    /// See [`DebugChannel::new`].
509    fn from_str(s: &str) -> Result<Self, Self::Err> {
510        Self::new(s)
511    }
512}
513
514impl FromStr for DllOverride {
515    type Err = FieldError;
516
517    /// See [`DllOverride::new`].
518    fn from_str(s: &str) -> Result<Self, Self::Err> {
519        Self::new(s)
520    }
521}
522
523impl FromStr for EnvName {
524    type Err = FieldError;
525
526    /// See [`EnvName::new`].
527    fn from_str(s: &str) -> Result<Self, Self::Err> {
528        Self::new(s)
529    }
530}
531
532impl fmt::Display for Fingerprint {
533    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
534        f.write_str(self.as_str())
535    }
536}
537
538impl fmt::Display for Argument {
539    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
540        f.write_str(self.as_str())
541    }
542}
543
544impl fmt::Display for DebugChannel {
545    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
546        f.write_str(self.as_str())
547    }
548}
549
550impl fmt::Display for DllOverride {
551    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
552        f.write_str(self.as_str())
553    }
554}
555
556impl fmt::Display for EnvName {
557    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
558        f.write_str(self.as_str())
559    }
560}
561
562impl AsRef<str> for Argument {
563    fn as_ref(&self) -> &str {
564        self.as_str()
565    }
566}
567
568impl PartialEq<&str> for Fingerprint {
569    fn eq(&self, other: &&str) -> bool {
570        self.0 == *other
571    }
572}
573
574impl PartialEq<&str> for Argument {
575    fn eq(&self, other: &&str) -> bool {
576        self.0 == *other
577    }
578}
579
580impl PartialEq<&str> for DebugChannel {
581    fn eq(&self, other: &&str) -> bool {
582        self.0 == *other
583    }
584}
585
586impl PartialEq<&str> for DllOverride {
587    fn eq(&self, other: &&str) -> bool {
588        self.0 == *other
589    }
590}
591
592impl PartialEq<&str> for EnvName {
593    fn eq(&self, other: &&str) -> bool {
594        self.0 == *other
595    }
596}
597
598#[cfg(test)]
599mod tests;