Skip to main content

compatdb_core/runtime/
process.rs

1//! The process side: its arch, and the UTF-16 image path, command line and
2//! environment block as wine hands them over.
3
4use crate::{EnvEntry, part::steam_app_id};
5
6/// The variable of the environment block that holds a process's Steam app id,
7/// which [`steam_app`] reads.
8pub const STEAM_APP_VAR: &str = "SteamAppId";
9
10/// The architecture of the program a process runs, which is that of its main
11/// executable.
12///
13/// Wine itself, the unix side with `ntdll.so` and this library, is built for
14/// arm64 and runs natively; x86 code is emulated inside the process.
15#[derive(Clone, Copy, PartialEq, Eq, Debug)]
16pub enum Arch {
17    /// A 64-bit x86 program. It runs in an ARM64EC process with its x86 code
18    /// emulated.
19    X86_64,
20    /// A 32-bit x86 program, run through wow64 with its x86 code emulated.
21    I386,
22    /// A native ARM64 program.
23    Aarch64,
24}
25
26impl Arch {
27    /// The `<arch>-windows` builtin directory name. The builtins of an
28    /// `x86_64` process load from `aarch64-windows`, as upstream Wine does on
29    /// an arm64 host: they are ARM64X modules that serve the process's
30    /// ARM64EC side.
31    #[must_use]
32    pub const fn pe_dir(self) -> &'static str {
33        match self {
34            Self::I386 => "i386-windows",
35            Self::X86_64 | Self::Aarch64 => "aarch64-windows",
36        }
37    }
38
39    /// The arch of the current process. `wow64` says whether it is a wow64
40    /// (32-bit) process; `machine` is the `Machine` field of its main image's
41    /// COFF header, when it could be read.
42    ///
43    /// The bundle only runs x86 programs under wow64, so a wow64 process is
44    /// i386. A 64-bit process is always native from Wine's side (ARM64EC for
45    /// an `x86_64` program), so the main image decides:
46    /// `IMAGE_FILE_MACHINE_ARM64` is a native ARM64 program, and anything
47    /// else, an unreadable header included, counts as `x86_64`. An ARM64EC
48    /// executable carries the AMD64 machine in its header and so also counts
49    /// as `x86_64`, which is what it presents itself as.
50    #[must_use]
51    pub const fn of_process(wow64: bool, machine: Option<u16>) -> Self {
52        match (wow64, machine) {
53            (true, _) => Self::I386,
54            (false, Some(IMAGE_FILE_MACHINE_ARM64)) => Self::Aarch64,
55            _ => Self::X86_64,
56        }
57    }
58}
59
60/// The COFF `Machine` value of an ARM64 image.
61pub const IMAGE_FILE_MACHINE_ARM64: u16 = 0xaa64;
62
63/// The basename of a DOS image path (`...\Foo\Bar.exe` -> `Bar.exe`), decoded
64/// lossily from UTF-16.
65#[must_use]
66pub fn basename_of(image_path: &[u16]) -> String {
67    let start = image_path
68        .iter()
69        .rposition(|&u| u == u16::from(b'\\') || u == u16::from(b'/'))
70        .map_or(0, |i| i.saturating_add(1));
71    String::from_utf16_lossy(image_path.get(start..).unwrap_or(&[]))
72}
73
74/// Append each text to the command line unless it is already present.
75///
76/// The substring test is case-sensitive, the rule that leaves Chromium
77/// `--type=` children, which inherit their parent's switches, untouched.
78/// Returns `None` when nothing changed.
79#[must_use]
80pub fn append_cmdline(cmdline: &[u16], appends: &[impl AsRef<str>]) -> Option<Vec<u16>> {
81    append_cmdline_each(cmdline, appends).0
82}
83
84/// [`append_cmdline`], also saying for each text whether it was appended:
85/// false for one that was already present, or empty.
86#[must_use]
87pub fn append_cmdline_each(
88    cmdline: &[u16],
89    appends: &[impl AsRef<str>],
90) -> (Option<Vec<u16>>, Vec<bool>) {
91    let mut cur = cmdline.to_vec();
92    let mut appended = Vec::with_capacity(appends.len());
93    for text in appends {
94        let text = text.as_ref();
95        if text.is_empty() || contains_cs(&cur, text) {
96            appended.push(false);
97            continue;
98        }
99        cur.push(u16::from(b' '));
100        cur.extend(text.encode_utf16());
101        appended.push(true);
102    }
103    (appended.contains(&true).then_some(cur), appended)
104}
105
106/// The value of the variable `name` in a `NAME=value\0...\0\0` block, the
107/// first entry for it, matched case-insensitively and decoded lossily; `None`
108/// when the block has none.
109#[must_use]
110pub fn env_value(block: &[u16], name: &str) -> Option<String> {
111    let seg = split_env(block)
112        .into_iter()
113        .find(|seg| env_name_matches(seg, name))?;
114    let value = seg
115        .iter()
116        .position(|&u| u == u16::from(b'='))
117        .and_then(|i| seg.get(i.saturating_add(1)..))
118        .unwrap_or(&[]);
119    Some(String::from_utf16_lossy(value))
120}
121
122/// The Steam app id in a `NAME=value\0...\0\0` block, the value of
123/// [`STEAM_APP_VAR`]. A value that is empty, 0 or no number is no app id:
124/// Steam's own processes have the variable empty.
125///
126/// It has to come from the Windows block: Wine does not copy that to the unix
127/// environment, so `getenv` does not see what Steam sets for a game.
128#[must_use]
129pub fn steam_app(block: &[u16]) -> Option<u32> {
130    steam_app_id(&env_value(block, STEAM_APP_VAR)?)
131}
132
133/// Merge environment entries into a `NAME=value\0...\0\0` block.
134///
135/// A name is matched case-insensitively; a value replaces in place or
136/// appends, an entry without one removes. Drive entries (`=C:=...`) are left
137/// alone. Returns `None` when nothing changed.
138#[must_use]
139pub fn merge_env(block: &[u16], entries: &[EnvEntry]) -> Option<Vec<u16>> {
140    let mut segs = split_env(block);
141    let mut changed = false;
142    for entry in entries {
143        let name = entry.name().as_str();
144        let found = segs.iter().position(|seg| env_name_matches(seg, name));
145        let Some(value) = entry.value() else {
146            if let Some(i) = found {
147                segs.remove(i);
148                changed = true;
149            }
150            continue;
151        };
152        let mut new_seg: Vec<u16> = name.encode_utf16().collect();
153        new_seg.push(u16::from(b'='));
154        new_seg.extend(value.encode_utf16());
155        match found {
156            Some(i) if segs.get(i) == Some(&new_seg) => {}
157            Some(i) => {
158                if let Some(slot) = segs.get_mut(i) {
159                    *slot = new_seg;
160                    changed = true;
161                }
162            }
163            None => {
164                segs.push(new_seg);
165                changed = true;
166            }
167        }
168    }
169    if !changed {
170        return None;
171    }
172    let mut out = Vec::new();
173    for seg in &segs {
174        out.extend_from_slice(seg);
175        out.push(0);
176    }
177    out.push(0);
178    Some(out)
179}
180
181/// The UTF-16 unit `u` in lower case when it is an ASCII capital letter.
182pub const fn to_lower_u16(u: u16) -> u16 {
183    if u >= b'A' as u16 && u <= b'Z' as u16 {
184        u | 0x20
185    } else {
186        u
187    }
188}
189
190/// Case-sensitive substring test.
191fn contains_cs(haystack: &[u16], needle: &str) -> bool {
192    let n: Vec<u16> = needle.encode_utf16().collect();
193    if n.is_empty() {
194        return true;
195    }
196    if haystack.len() < n.len() {
197        return false;
198    }
199    haystack.windows(n.len()).any(|w| w == n.as_slice())
200}
201
202/// Split an environment block into its `NAME=value` segments, dropping the
203/// terminating NULs.
204fn split_env(block: &[u16]) -> Vec<Vec<u16>> {
205    let mut segs = Vec::new();
206    let mut cur = Vec::new();
207    for &u in block {
208        if u == 0 {
209            if cur.is_empty() {
210                break;
211            }
212            segs.push(std::mem::take(&mut cur));
213        } else {
214            cur.push(u);
215        }
216    }
217    segs
218}
219
220/// Whether an env segment's name equals `name` case-insensitively, skipping
221/// the `=C:` drive entries.
222fn env_name_matches(seg: &[u16], name: &str) -> bool {
223    if seg.first() == Some(&u16::from(b'=')) {
224        return false;
225    }
226    let seg_name = seg.split(|&u| u == u16::from(b'=')).next().unwrap_or(seg);
227    let want: Vec<u16> = name.encode_utf16().map(to_lower_u16).collect();
228    seg_name.len() == want.len()
229        && seg_name
230            .iter()
231            .map(|&u| to_lower_u16(u))
232            .eq(want.iter().copied())
233}
234
235#[cfg(test)]
236mod tests;