winbrew_windows\deployment\msi/
database.rs

1//! Low-level MSI database access and row loading.
2//!
3//! This module is the only place that talks directly to the Windows Installer
4//! C API. It owns the handle lifecycle, the query/view execution flow, and the
5//! strict UTF-16 conversion used for MSI string fields.
6//!
7//! Two implementation details are worth keeping visible here:
8//!
9//! - MSI string extraction uses a probe call first, so the real buffer can be
10//!   sized from the length reported by the API.
11//! - `MsiRecordIsNull` is treated differently from an empty string, because the
12//!   scanner needs to preserve that distinction when normalizing rows.
13
14use anyhow::{Context, Result, bail};
15use std::collections::HashMap;
16use std::os::windows::ffi::OsStrExt;
17use std::path::Path;
18use windows::Win32::Foundation::{ERROR_MORE_DATA, ERROR_NO_MORE_ITEMS, ERROR_SUCCESS};
19use windows::Win32::System::ApplicationInstallationAndServicing::{
20    MSIDBOPEN_READONLY, MSIHANDLE, MsiCloseHandle, MsiDatabaseOpenViewW, MsiOpenDatabaseW,
21    MsiRecordGetInteger, MsiRecordGetStringW, MsiRecordIsNull, MsiViewExecute, MsiViewFetch,
22};
23use windows::core::{HSTRING, PCWSTR, PWSTR};
24
25use super::{ComponentRow, DirectoryRow, FileRow, RegistryRow, ShortcutRow};
26
27pub(super) struct MsiDatabase(MsiHandle);
28
29impl MsiDatabase {
30    /// Open the MSI database in read-only mode and keep the handle RAII-owned.
31    pub(super) fn open(path: &Path) -> Result<Self> {
32        let wide_path = wide_path(path);
33        let mut handle = MSIHANDLE(0);
34        let status = unsafe {
35            MsiOpenDatabaseW(PCWSTR(wide_path.as_ptr()), MSIDBOPEN_READONLY, &mut handle)
36        };
37
38        ensure_msi_success(status, "open MSI database")?;
39
40        Ok(Self(MsiHandle::new(handle)))
41    }
42
43    /// Borrow the raw MSI handle for query helpers.
44    pub(super) fn handle(&self) -> MSIHANDLE {
45        self.0.raw()
46    }
47}
48
49/// RAII owner for `MSIHANDLE` values.
50///
51/// The underlying MSI API uses manual handle management; this wrapper ensures
52/// that every successfully opened database or view gets closed on drop.
53#[derive(Debug)]
54struct MsiHandle(MSIHANDLE);
55
56impl MsiHandle {
57    fn new(handle: MSIHANDLE) -> Self {
58        Self(handle)
59    }
60
61    fn raw(&self) -> MSIHANDLE {
62        self.0
63    }
64}
65
66impl Drop for MsiHandle {
67    fn drop(&mut self) {
68        if self.0.0 != 0 {
69            unsafe {
70                let _ = MsiCloseHandle(self.0);
71            }
72        }
73    }
74}
75
76pub(super) fn load_directory_rows(database: MSIHANDLE) -> Result<HashMap<String, DirectoryRow>> {
77    // Load the `Directory` table into a map keyed by directory id.
78    let rows = collect_rows(
79        database,
80        "SELECT `Directory`, `Directory_Parent`, `DefaultDir` FROM `Directory`",
81        |record| {
82            Ok((
83                record_string(record, 1)?,
84                DirectoryRow {
85                    parent: record_optional_string(record, 2)?,
86                    default_dir: record_string(record, 3)?,
87                },
88            ))
89        },
90    )?;
91
92    Ok(rows.into_iter().collect())
93}
94
95pub(super) fn load_component_rows(database: MSIHANDLE) -> Result<HashMap<String, ComponentRow>> {
96    // Load the `Component` table into a map keyed by component id.
97    let rows = collect_rows(
98        database,
99        "SELECT `Component`, `Directory_`, `KeyPath` FROM `Component`",
100        |record| {
101            Ok((
102                record_string(record, 1)?,
103                ComponentRow {
104                    directory_id: record_string(record, 2)?,
105                    key_path: record_optional_string(record, 3)?,
106                },
107            ))
108        },
109    )?;
110
111    Ok(rows.into_iter().collect())
112}
113
114pub(super) fn load_file_rows(database: MSIHANDLE) -> Result<Vec<FileRow>> {
115    // Load the `File` table in table order.
116    collect_rows(
117        database,
118        "SELECT `File`, `Component_`, `FileName` FROM `File`",
119        |record| {
120            Ok(FileRow {
121                file_key: record_string(record, 1)?,
122                component_id: record_string(record, 2)?,
123                file_name: record_string(record, 3)?,
124            })
125        },
126    )
127}
128
129pub(super) fn load_registry_rows(database: MSIHANDLE) -> Result<Vec<RegistryRow>> {
130    // Load the `Registry` table in table order.
131    collect_rows(
132        database,
133        "SELECT `Root`, `Key`, `Name`, `Value` FROM `Registry`",
134        |record| {
135            Ok(RegistryRow {
136                root: record_integer(record, 1),
137                key_path: record_string(record, 2)?,
138                name: record_optional_string(record, 3)?,
139                value: record_optional_string(record, 4)?,
140            })
141        },
142    )
143}
144
145pub(super) fn load_shortcut_rows(database: MSIHANDLE) -> Result<Vec<ShortcutRow>> {
146    // Load the `Shortcut` table in table order.
147    collect_rows(
148        database,
149        "SELECT `Directory_`, `Name`, `Target` FROM `Shortcut`",
150        |record| {
151            Ok(ShortcutRow {
152                directory_id: record_string(record, 1)?,
153                name: record_string(record, 2)?,
154                target: record_string(record, 3)?,
155            })
156        },
157    )
158}
159
160pub(super) fn query_required_string(database: MSIHANDLE, query: &str) -> Result<String> {
161    // Execute a scalar query and fail if it returns no rows.
162    query_optional_string(database, query)?
163        .with_context(|| format!("missing MSI query result for '{query}'"))
164}
165
166pub(super) fn query_optional_string(database: MSIHANDLE, query: &str) -> Result<Option<String>> {
167    // Execute a scalar query and return the first row if one exists.
168    let rows = collect_rows(database, query, |record| record_string(record, 1))?;
169
170    Ok(rows.into_iter().next())
171}
172
173fn collect_rows<T, F>(database: MSIHANDLE, query: &str, mut parse_row: F) -> Result<Vec<T>>
174where
175    F: FnMut(MSIHANDLE) -> Result<T>,
176{
177    // Execute a view and collect every fetched record through a parser.
178    let view = open_view(database, query)?;
179    let view = MsiHandle::new(view);
180    execute_view(view.raw())?;
181
182    let mut rows = Vec::new();
183
184    loop {
185        let mut record = MSIHANDLE(0);
186        let status = unsafe { MsiViewFetch(view.raw(), &mut record) };
187
188        if status == ERROR_NO_MORE_ITEMS.0 || record.0 == 0 {
189            break;
190        }
191
192        ensure_msi_success(status, "fetch MSI record")?;
193
194        let record = MsiHandle::new(record);
195        rows.push(parse_row(record.raw())?);
196    }
197
198    Ok(rows)
199}
200
201fn open_view(database: MSIHANDLE, query: &str) -> Result<MSIHANDLE> {
202    // Open an MSI view for a textual query.
203    let query = HSTRING::from(query);
204    let mut view = MSIHANDLE(0);
205    let status = unsafe { MsiDatabaseOpenViewW(database, &query, &mut view) };
206
207    ensure_msi_success(status, "open MSI view")?;
208
209    Ok(view)
210}
211
212fn execute_view(view: MSIHANDLE) -> Result<()> {
213    // Execute a previously opened view with the default null record.
214    let status = unsafe { MsiViewExecute(view, MSIHANDLE(0)) };
215
216    ensure_msi_success(status, "execute MSI view")
217}
218
219fn record_optional_string(record: MSIHANDLE, field: u32) -> Result<Option<String>> {
220    // Read an optional MSI string field while preserving null-vs-empty.
221    if unsafe { MsiRecordIsNull(record, field).as_bool() } {
222        return Ok(None);
223    }
224
225    record_string(record, field).map(Some)
226}
227
228fn record_string(record: MSIHANDLE, field: u32) -> Result<String> {
229    // Read an MSI string field using the API's probe-and-read pattern.
230    //
231    // The first call asks the MSI runtime for the required character count.
232    // The second call reads the actual UTF-16 payload, which is then decoded
233    // strictly so invalid data is surfaced instead of being lossy-converted.
234    let mut probe = [0u16; 1];
235    let mut length = 0u32;
236    let status = unsafe {
237        MsiRecordGetStringW(
238            record,
239            field,
240            Some(PWSTR(probe.as_mut_ptr())),
241            Some(&mut length),
242        )
243    };
244
245    if status == ERROR_SUCCESS.0 {
246        return Ok(String::new());
247    }
248
249    if status != ERROR_MORE_DATA.0 {
250        ensure_msi_success(status, "probe MSI record string")?;
251    }
252
253    let mut buffer = vec![0u16; length as usize + 1];
254    let mut written = buffer.len() as u32;
255    let status = unsafe {
256        MsiRecordGetStringW(
257            record,
258            field,
259            Some(PWSTR(buffer.as_mut_ptr())),
260            Some(&mut written),
261        )
262    };
263    ensure_msi_success(status, "read MSI record string")?;
264
265    buffer.truncate(written as usize);
266    String::from_utf16(&buffer).context("MSI record string contained invalid UTF-16")
267}
268
269fn record_integer(record: MSIHANDLE, field: u32) -> i32 {
270    // Read an MSI integer field.
271    unsafe { MsiRecordGetInteger(record, field) }
272}
273
274fn ensure_msi_success(status: u32, context: &str) -> Result<()> {
275    // Convert a raw MSI status code into a contextual `anyhow::Result`.
276    if status == ERROR_SUCCESS.0 {
277        Ok(())
278    } else {
279        bail!("{context} failed with MSI error code {status}")
280    }
281}
282
283fn wide_path(path: &Path) -> Vec<u16> {
284    // Encode a Rust path as a null-terminated UTF-16 buffer for Windows APIs.
285    path.as_os_str().encode_wide().chain(Some(0)).collect()
286}