winbrew_windows\deployment\msi/
directory.rs

1//! Resolves MSI `Directory` rows into absolute install paths.
2//!
3//! This module is intentionally narrow: it only handles the directory graph,
4//! cycle detection, and the special handling for MSI's root-ish directory ids.
5//! If the database omits a required row or forms a loop, the scan fails with a
6//! contextual error instead of inventing a path.
7
8use anyhow::{Context, Result, bail};
9use std::collections::{HashMap, HashSet};
10use std::path::{Path, PathBuf};
11
12use super::{DirectoryRow, path::select_msi_name};
13
14pub(super) fn resolve_directory_paths(
15    rows: &HashMap<String, DirectoryRow>,
16    install_root: &Path,
17) -> Result<HashMap<String, PathBuf>> {
18    // Resolve every directory id in the table into a concrete path.
19    //
20    // The walk is memoized so each directory is resolved once, and a
21    // `visiting` set guards against recursive cycles in the MSI graph.
22    let mut resolved = HashMap::new();
23    let mut visiting = HashSet::new();
24
25    for directory_id in rows.keys() {
26        let _ = resolve_directory_path(
27            directory_id,
28            rows,
29            &mut resolved,
30            &mut visiting,
31            install_root,
32        )?;
33    }
34
35    Ok(resolved)
36}
37
38fn resolve_directory_path(
39    directory_id: &str,
40    rows: &HashMap<String, DirectoryRow>,
41    resolved: &mut HashMap<String, PathBuf>,
42    visiting: &mut HashSet<String>,
43    install_root: &Path,
44) -> Result<PathBuf> {
45    // Resolve one directory id, following parents first and anchoring the
46    // special MSI roots at the install root.
47    if let Some(path) = resolved.get(directory_id) {
48        return Ok(path.clone());
49    }
50
51    if !visiting.insert(directory_id.to_string()) {
52        bail!("cycle detected in MSI Directory table at '{directory_id}'");
53    }
54
55    let row = rows
56        .get(directory_id)
57        .with_context(|| format!("missing MSI Directory row for '{directory_id}'"))?;
58
59    let base = match row.parent.as_deref() {
60        Some(parent) if !parent.is_empty() => {
61            resolve_directory_path(parent, rows, resolved, visiting, install_root)?
62        }
63        _ if directory_id.eq_ignore_ascii_case("TARGETDIR")
64            || directory_id.eq_ignore_ascii_case("SOURCEDIR") =>
65        {
66            install_root.to_path_buf()
67        }
68        _ => install_root.to_path_buf(),
69    };
70
71    let path = match select_msi_name(&row.default_dir) {
72        Some(segment) => base.join(segment),
73        None => base,
74    };
75
76    visiting.remove(directory_id);
77    resolved.insert(directory_id.to_string(), path.clone());
78
79    Ok(path)
80}