winbrew_app\operations\repair/
resolution.rs

1use anyhow::{Context, Result};
2
3use crate::AppContext;
4use crate::catalog;
5use crate::database;
6use crate::engines::{self, EngineKind};
7use crate::models::catalog::{CatalogInstaller, CatalogPackage};
8use crate::models::domains::installed::InstalledPackage;
9use crate::models::domains::package::{PackageId, PackageRef};
10use crate::operations::install::{self, InstallObserver};
11use crate::operations::remove;
12
13#[derive(Debug, Clone)]
14pub struct ResolvedFileRestoreTarget {
15    pub package: CatalogPackage,
16    pub installer: CatalogInstaller,
17    pub engine: EngineKind,
18    pub installed_package: InstalledPackage,
19}
20
21#[derive(Debug, Clone)]
22pub struct FileRestoreReinstallTarget {
23    pub catalog_package: CatalogPackage,
24    pub installed_version: String,
25}
26
27#[derive(Debug, Clone)]
28pub enum FileRestoreResolution {
29    Restore(Box<ResolvedFileRestoreTarget>),
30    Reinstall(Box<FileRestoreReinstallTarget>),
31}
32
33/// Resolve a catalog package for repair using the same matching policy as install.
34pub fn resolve_repair_catalog_package<FChoose>(
35    package_name: &str,
36    choose_package: FChoose,
37) -> Result<CatalogPackage>
38where
39    FChoose: FnMut(&str, &[CatalogPackage]) -> Result<usize>,
40{
41    let catalog_conn = crate::database::get_catalog_conn()?;
42    resolve_repair_catalog_package_with_conn(&catalog_conn, package_name, choose_package)
43}
44
45fn resolve_repair_catalog_package_with_conn<FChoose>(
46    catalog_conn: &crate::database::DbConnection,
47    package_name: &str,
48    choose_package: FChoose,
49) -> Result<CatalogPackage>
50where
51    FChoose: FnMut(&str, &[CatalogPackage]) -> Result<usize>,
52{
53    let package_ref = PackageRef::parse(package_name)
54        .with_context(|| format!("failed to parse package reference '{package_name}'"))?;
55
56    catalog::resolve_catalog_package_ref(catalog_conn, &package_ref, choose_package)
57}
58
59/// Resolve a file-restore target and decide whether reinstall is required.
60pub fn resolve_file_restore_target<FChoose>(
61    package_name: &str,
62    choose_package: FChoose,
63) -> Result<FileRestoreResolution>
64where
65    FChoose: FnMut(&str, &[CatalogPackage]) -> Result<usize>,
66{
67    let catalog_conn = crate::database::get_catalog_conn()?;
68    let conn = database::get_conn()?;
69    let package =
70        resolve_repair_catalog_package_with_conn(&catalog_conn, package_name, choose_package)?;
71    let installed_package = database::get_package(&conn, package_name)?
72        .with_context(|| format!("package '{package_name}' is not installed"))?;
73
74    if installed_package.version != package.version.to_string() {
75        return Ok(FileRestoreResolution::Reinstall(Box::new(
76            FileRestoreReinstallTarget {
77                catalog_package: package,
78                installed_version: installed_package.version,
79            },
80        )));
81    }
82
83    let installers = crate::database::get_installers(&catalog_conn, &package.id)?;
84    let selection_context = crate::catalog::SelectionContext::new(
85        crate::windows::host::host_profile(),
86        crate::windows::host::is_elevated(),
87    );
88    let installer = install::types::select_installer(&installers, selection_context)?;
89    let engine = engines::resolve_engine_for_installer(&installer)?;
90
91    if engine_requires_reinstall_only(engine) {
92        return Ok(FileRestoreResolution::Reinstall(Box::new(
93            FileRestoreReinstallTarget {
94                catalog_package: package,
95                installed_version: installed_package.version,
96            },
97        )));
98    }
99
100    Ok(FileRestoreResolution::Restore(Box::new(
101        ResolvedFileRestoreTarget {
102            package,
103            installer,
104            engine,
105            installed_package,
106        },
107    )))
108}
109
110pub(crate) fn engine_requires_reinstall_only(engine: EngineKind) -> bool {
111    matches!(engine, EngineKind::Font)
112}
113
114/// Reinstall a package using the exact catalog package that was already chosen.
115pub fn reinstall_package<O: InstallObserver>(
116    ctx: &AppContext,
117    catalog_package: &CatalogPackage,
118    observer: &mut O,
119) -> Result<install::InstallOutcome> {
120    let conn = database::get_conn()?;
121
122    if database::get_package(&conn, &catalog_package.name)?.is_some() {
123        remove::remove(&catalog_package.name, true).with_context(|| {
124            format!(
125                "failed to remove package before repair: {}",
126                catalog_package.name
127            )
128        })?;
129    }
130
131    let package_ref = PackageRef::ById(
132        PackageId::parse(catalog_package.id.as_str())
133            .with_context(|| format!("failed to parse catalog id '{}'", catalog_package.id))?,
134    );
135
136    install::run(ctx, package_ref, false, observer)
137        .with_context(|| format!("failed to reinstall package '{}'", catalog_package.name))
138}
139
140#[cfg(test)]
141mod tests {
142    use super::engine_requires_reinstall_only;
143    use crate::engines::EngineKind;
144
145    #[test]
146    fn engine_requires_reinstall_only_returns_true_for_fonts() {
147        assert!(engine_requires_reinstall_only(EngineKind::Font));
148        assert!(!engine_requires_reinstall_only(EngineKind::NativeExe));
149        assert!(!engine_requires_reinstall_only(EngineKind::Portable));
150    }
151}