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