main
Drew Owens 0.10.1 source afb9926 8d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890
//! Find SOLIDWORKS installs and running instances (Windows).
//!
//! Detection ONLY. ab's rule for host apps is detect-by-default, install
//! on-request — and SOLIDWORKS has no silent free installer anyway (desktop
//! trial serials come from a reseller), so the bridge reports absence with next
//! steps instead of ever attempting an install itself.
//!
//! Three signals, cheapest and most reliable first:
//!   1. the `App Paths\SLDWORKS.exe` registry key (HKLM then HKCU),
//!   2. the known install-path shapes under Program Files,
//!   3. `HKLM\SOFTWARE\SolidWorks` subkeys for the version census,
//! plus a `tasklist` check for whether it is running right now.

use serde_json::{json, Value};

#[derive(Clone, Debug, Default)]
pub struct Detection {
    pub installed: bool,
    pub exe: Option<String>,
    pub registry_versions: Vec<String>,
    pub running: bool,
    /// Is the 3DEXPERIENCE Launcher present on this machine? A SECOND,
    /// independent signal for [`Detection::edition`] — the install path alone is
    /// a substring heuristic, and getting the edition wrong is what puts the
    /// "launch me from the Platform" modal back on the user's screen.
    pub launcher_present: bool,
    /// The Platform-created shortcut that STARTS SOLIDWORKS Connected, if this
    /// machine has one. See [`find_shortcut`] — this is the field that turned
    /// "cannot be launched programmatically" into a launch that works.
    pub shortcut: Option<String>,
}

impl Detection {
    pub fn to_json(&self) -> Value {
        let edition = self.edition();
        json!({
            "installed": self.installed,
            "exe": self.exe,
            "registryVersions": self.registry_versions,
            "running": self.running,
            "platform": std::env::consts::OS,
            "edition": edition,
            "canLaunchProgrammatically": self.can_launch(),
            "shortcut": self.shortcut,
            "launchRoute": self.launch_route().name(),
            // CLASSIC DESKTOP RELEASES in the registry (0.10.0): SOLIDWORKS 2021
            // is major version 29. Launched through COM (the exe), with no
            // 3DEXPERIENCE sign-in; see docs/COMPATIBILITY.md.
            "registryYears": registry_years(&self.registry_versions),
            "classicYears": classic_years_from(self.exe.as_deref(), &self.registry_versions, edition),
        })
    }

    /// HOW this bridge may start SOLIDWORKS, if it may at all.
    pub fn launch_route(&self) -> LaunchRoute {
        launch_route_from(self.installed, self.edition(), self.shortcut.as_deref())
    }

    /// Which SOLIDWORKS this is: `"connected"`, `"desktop"` or `"unknown"`.
    ///
    /// The distinction is not cosmetic — it decides whether `swx_start` may
    /// launch the application at all.
    ///
    /// LIVE FINDING (2026-09-02): on **SOLIDWORKS Connected** (the
    /// 3DEXPERIENCE-hosted editions, which includes "Design Professional for
    /// Makers"), `CoCreateInstance(SldWorks.Application)` starts a process that
    /// immediately puts up *"SOLIDWORKS Design must be launched from the
    /// 3DEXPERIENCE Platform or from the desktop shortcut created from the
    /// Platform"* and never becomes a drivable COM server. Launching is
    /// human-gated by Dassault. Classic desktop SOLIDWORKS is unaffected, so the
    /// launch path stays — behind this check.
    ///
    /// THREE independent signals, because the first version of this was a single
    /// substring test on an install path nobody had ever observed on the target
    /// machine, and a wrong answer means firing the launch that produces that
    /// modal. `"desktop"` now requires POSITIVE evidence; anything unrecognised
    /// is `"unknown"`, and unknown does not launch.
    pub fn edition(&self) -> &'static str {
        edition_from(self.exe.as_deref(), &self.registry_versions, self.launcher_present)
    }

    /// May this bridge start SOLIDWORKS itself?
    ///
    /// ONLY a positively-identified classic desktop install. An unrecognised
    /// layout answers NO: the cost of refusing a launch we could have made is
    /// one sentence asking the user to click their own shortcut, and the cost of
    /// making a launch we should have refused is a modal dialog and a dead
    /// process on their machine. Those are not close.
    pub fn can_launch(&self) -> bool {
        self.installed && self.edition() == "desktop"
    }
}

/// The edition decision as a PURE function, so both install layouts can be
/// unit-tested on any host — the previous version could only be exercised on a
/// Windows box with SOLIDWORKS actually installed, which is to say never.
///
/// Evidence, in order:
///   1. an install path or registry entry naming 3DEXPERIENCE  -> connected
///   2. the 3DEXPERIENCE Launcher present on the machine       -> connected
///   3. an install path under `SOLIDWORKS Corp` (the classic
///      desktop layout, and the ONLY positive evidence for it) -> desktop
///   4. anything else                                          -> unknown
pub fn edition_from(
    exe: Option<&str>,
    registry_versions: &[String],
    launcher_present: bool,
) -> &'static str {
    let up = exe.unwrap_or("").to_uppercase();
    let registry_says_connected = registry_versions
        .iter()
        .any(|v| v.to_uppercase().contains("3DEXPERIENCE") || v.to_uppercase().contains("CONNECTED"));
    if up.contains("3DEXPERIENCE") || registry_says_connected || launcher_present {
        return "connected";
    }
    if up.contains("SOLIDWORKS CORP") {
        return "desktop";
    }
    // THE CLASSIC REGISTRY LAYOUT (0.10.0, issue #1/#2: SOLIDWORKS 2021):
    // `HKLM\SOFTWARE\SolidWorks\SOLIDWORKS 2021`, a year and nothing else, with no
    // 3DEXPERIENCE marker anywhere (checked above) and an exe found. UNMEASURED
    // on a classic machine; it is the documented layout of the desktop installer.
    if exe.is_some() && !registry_years(registry_versions).is_empty() {
        return "desktop";
    }
    if exe.is_none() {
        return "unknown";
    }
    // A path we do not recognise — including `Dassault Systemes\...` without any
    // other marker, which is where the Connected editions live. Not proof of
    // either, so: unknown, which means "do not launch".
    "unknown"
}

/// Every release year a plain `SOLIDWORKS <year>` registry key names:
/// "SOLIDWORKS 2021" -> 2021. A FACT about the registry, not a claim about the
/// edition — MEASURED 2026-10-13: the 3DEXPERIENCE (Connected) 2026 install
/// writes "SOLIDWORKS 2026" too, so this alone is not "a classic install".
pub fn registry_years(registry_versions: &[String]) -> Vec<u32> {
    let mut out: Vec<u32> = registry_versions
        .iter()
        .filter_map(|v| {
            let rest = v.trim().strip_prefix("SOLIDWORKS ").or_else(|| v.trim().strip_prefix("SolidWorks "))?;
            let year: u32 = rest.trim().parse().ok()?;
            (2000..2100).contains(&year).then_some(year)
        })
        .collect();
    out.sort();
    out.dedup();
    out
}

/// The release years tied to 3DEXPERIENCE / Connected: a registry entry or an
/// install path that names the platform and a year ("SOLIDWORKS 3DEXPERIENCE
/// R2026x" -> 2026).
pub fn connected_years(exe: Option<&str>, registry_versions: &[String]) -> Vec<u32> {
    fn years_in(s: &str) -> Vec<u32> {
        let b = s.as_bytes();
        let mut out = Vec::new();
        let mut i = 0;
        while i + 4 <= b.len() {
            let run = &b[i..i + 4];
            let boundary_before = i == 0 || !b[i - 1].is_ascii_digit();
            let boundary_after = i + 4 == b.len() || !b[i + 4].is_ascii_digit();
            if run.iter().all(|c| c.is_ascii_digit()) && boundary_before && boundary_after {
                if let Ok(y) = std::str::from_utf8(run).unwrap_or("").parse::<u32>() {
                    if (2000..2100).contains(&y) {
                        out.push(y);
                    }
                }
                i += 4;
            } else {
                i += 1;
            }
        }
        out
    }
    let marked = |t: &str| {
        let u = t.to_uppercase();
        u.contains("3DEXPERIENCE") || u.contains("CONNECTED")
    };
    let mut out: Vec<u32> = registry_versions.iter().filter(|v| marked(v)).flat_map(|v| years_in(v)).collect();
    if let Some(e) = exe.filter(|e| marked(e)) {
        out.extend(years_in(e));
    }
    out.sort();
    out.dedup();
    out
}

/// The CLASSIC desktop releases on this machine: registry years with no
/// 3DEXPERIENCE entry or install path for the same release. On a Connected
/// machine whose platform marker names no year (the launcher alone), no year
/// can be shown to be classic, so none is claimed. MEASURED 2026-10-13: 0.10.0
/// reported `classicYears: [2026]` on the 3DEXPERIENCE 2026 install, which
/// read as a classic install that is not there. The EDITION decision is not
/// made here and did not change.
pub fn classic_years_from(exe: Option<&str>, registry_versions: &[String], edition: &str) -> Vec<u32> {
    let all = registry_years(registry_versions);
    if edition == "desktop" {
        return all;
    }
    let connected = connected_years(exe, registry_versions);
    if edition == "connected" && connected.is_empty() {
        return Vec::new();
    }
    all.into_iter().filter(|y| !connected.contains(y)).collect()
}

/// How this bridge may start SOLIDWORKS.
///
/// LIVE FINDING (2026-09-03) — THE LAUNCH ANSWER. The earlier conclusion that
/// SOLIDWORKS Connected "cannot be launched programmatically" was drawn from the
/// wrong experiment: we called `CoCreateInstance` on `SldWorks.Application`,
/// which starts `sldworks.exe` DIRECTLY, and the application correctly refused
/// because it had not been given a tenant or a registry to talk to. Reading the
/// Start-Menu shortcut the Platform installs shows what the sanctioned start
/// actually is, and it is not sldworks.exe at all:
///
/// ```text
/// CATSTART.exe -run "SWXDesktopLauncher.exe"
///              -object "--AppName=\"SWXCSWK_AP\" -tenant=<TENANT> -monoapp
///                       -3DRegistryURL=https://<region>-makers-registry.3dexperience.3ds.com"
///              -nowindow
/// ```
///
/// So the refusal was ours: we bypassed the launcher chain. Invoking the
/// USER'S OWN SHORTCUT runs exactly the command Dassault intends, with the
/// tenant and registry arguments the Platform baked into it — nothing is
/// reverse-engineered and no argument is invented. That is why this bridge
/// invokes the `.lnk` rather than assembling a CATSTART command line itself: the
/// shortcut is the sanctioned artefact, and a hand-built command line would be a
/// guess at somebody's tenant.
#[derive(Clone, Debug, PartialEq)]
pub enum LaunchRoute {
    /// Classic desktop SOLIDWORKS: `CoCreateInstance` yields a drivable COM
    /// server on its own.
    Com,
    /// SOLIDWORKS Connected: invoke this shortcut and wait for the COM server to
    /// appear. Cold start runs minutes, not seconds.
    PlatformShortcut(String),
    /// Nothing this bridge may do — ask the user to start it.
    Blocked,
}

impl LaunchRoute {
    pub fn name(&self) -> &'static str {
        match self {
            LaunchRoute::Com => "com",
            LaunchRoute::PlatformShortcut(_) => "platformShortcut",
            LaunchRoute::Blocked => "blocked",
        }
    }
}

/// The route decision as a PURE function, so every branch is reachable from a
/// test on any host.
///
/// `installed` is deliberately NOT required for the shortcut route: a shortcut
/// that exists IS evidence of an install, and the exe globs miss the Connected
/// layouts often enough that requiring both would refuse launches on exactly the
/// machines this route exists for.
pub fn launch_route_from(
    installed: bool,
    edition: &'static str,
    shortcut: Option<&str>,
) -> LaunchRoute {
    // A positively-identified desktop install attaches directly, which is
    // faster and needs no external process. Prefer it.
    if installed && edition == "desktop" {
        return LaunchRoute::Com;
    }
    match shortcut.filter(|s| !s.is_empty()) {
        Some(s) => LaunchRoute::PlatformShortcut(s.to_string()),
        None => LaunchRoute::Blocked,
    }
}

/// An explicit override, for a machine whose shortcut lives somewhere these
/// globs do not look. Also how the Linux test suite drives the shortcut route.
pub const ENV_SHORTCUT: &str = "SWX_SOLIDWORKS_SHORTCUT";

/// Folders the Platform puts its Start-Menu group in. The `*` matches the
/// release component (`... 3DEXPERIENCE R2026x`).
pub const SHORTCUT_DIR_GLOBS: &[&str] = &[
    r"%ProgramData%\Microsoft\Windows\Start Menu\Programs\Dassault Systemes SOLIDWORKS 3DEXPERIENCE*",
    r"%APPDATA%\Microsoft\Windows\Start Menu\Programs\Dassault Systemes SOLIDWORKS 3DEXPERIENCE*",
    r"%ProgramData%\Microsoft\Windows\Start Menu\Programs\Dassault Systemes SOLIDWORKS*",
    r"%APPDATA%\Microsoft\Windows\Start Menu\Programs\Dassault Systemes SOLIDWORKS*",
];

/// Individual shortcuts, checked before the folder scan. The Platform also
/// offers to drop one on the desktop, and the refusal modal names it
/// ("...or from the desktop shortcut created from the Platform").
pub const SHORTCUT_FILE_GLOBS: &[&str] = &[
    r"%ProgramData%\Microsoft\Windows\Start Menu\Programs\Dassault Systemes SOLIDWORKS 3DEXPERIENCE*\SOLIDWORKS Design.lnk",
    r"%APPDATA%\Microsoft\Windows\Start Menu\Programs\Dassault Systemes SOLIDWORKS 3DEXPERIENCE*\SOLIDWORKS Design.lnk",
    r"%USERPROFILE%\Desktop\SOLIDWORKS Design.lnk",
    r"%PUBLIC%\Desktop\SOLIDWORKS Design.lnk",
];

/// Names in the Start-Menu group that are NOT the application. Launching one of
/// these would run an uninstaller or a side tool — so the ranking rejects rather
/// than guesses, and `Blocked` (which asks the user) is the fallback.
const NOT_THE_APP: &[&str] = &[
    "uninstall", "installation", "install manager", "readme", "release notes", "license",
    "administrat", "rx", "explorer", "toolbox", "settings", "manage", "utilit", "sldim",
    "composer", "visualize", "electrical", "inspection", "pdm", "simulation", "documentation",
    "help", "what's new", "tutorial", "check", "task scheduler", "language",
];

/// Which of several candidate shortcuts is the one that starts SOLIDWORKS.
///
/// Pure and ranked rather than "first match": a Start-Menu group holds an
/// uninstaller and a pile of side tools alongside the application, and picking
/// the wrong one would run the wrong program on somebody's machine.
pub fn pick_shortcut(candidates: &[String]) -> Option<String> {
    let score = |p: &String| -> Option<i32> {
        let file = p.rsplit(|c| c == '\\' || c == '/').next().unwrap_or("").to_lowercase();
        if !file.ends_with(".lnk") {
            return None;
        }
        let stem = file.trim_end_matches(".lnk");
        if !stem.contains("solidworks") {
            return None;
        }
        if NOT_THE_APP.iter().any(|bad| stem.contains(bad)) {
            return None;
        }
        // Exact, then the Connected application name, then anything left —
        // shorter names first, because the plain application is always the
        // shortest "SOLIDWORKS ..." entry in the group.
        Some(match stem {
            "solidworks design" => 0,
            "solidworks" => 1,
            s if s.contains("design") => 2,
            s => 3 + s.len() as i32,
        })
    };
    candidates
        .iter()
        .filter_map(|c| score(c).map(|s| (s, c)))
        .min_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(b.1)))
        .map(|(_, c)| c.clone())
}

/// Find the shortcut that starts SOLIDWORKS Connected, if there is one.
pub fn find_shortcut() -> Option<String> {
    if let Ok(explicit) = std::env::var(ENV_SHORTCUT) {
        if !explicit.is_empty() {
            // An explicit override is honoured whether or not it exists yet, so
            // a wrong value fails loudly at launch instead of silently falling
            // back to a different shortcut.
            return Some(explicit);
        }
    }
    let mut candidates: Vec<String> =
        SHORTCUT_FILE_GLOBS.iter().flat_map(|g| expand_glob(g)).collect();
    if let Some(found) = pick_shortcut(&candidates) {
        return Some(found);
    }
    // Nothing at the known names: scan the group folder itself.
    for dir_glob in SHORTCUT_DIR_GLOBS {
        for dir in expand_dir_glob(dir_glob) {
            if let Ok(entries) = std::fs::read_dir(&dir) {
                for e in entries.flatten() {
                    candidates.push(e.path().to_string_lossy().to_string());
                }
            }
        }
    }
    pick_shortcut(&candidates)
}

/// [`expand_glob`] for a pattern whose final component is a DIRECTORY.
/// `expand_glob` requires the resolved candidate to be a file, so it cannot
/// answer "which release folder is on this machine".
pub fn expand_dir_glob(pattern: &str) -> Vec<String> {
    let expanded = expand_env(pattern);
    let sep = |c: char| c == '\\' || c == '/';
    let star = match expanded.find('*') {
        None => {
            return if std::path::Path::new(&expanded).is_dir() { vec![expanded] } else { vec![] }
        }
        Some(i) => i,
    };
    let (head, tail) = (&expanded[..star], &expanded[star + 1..]);
    if tail.contains(|c| sep(c)) {
        return Vec::new(); // one wildcard component, and it is the last one
    }
    let (parent, prefix) = match head.rfind(sep) {
        Some(i) => (&head[..i], &head[i + 1..]),
        None => ("", head),
    };
    let mut hits = Vec::new();
    if let Ok(entries) = std::fs::read_dir(if parent.is_empty() { "." } else { parent }) {
        for e in entries.flatten() {
            if !e.path().is_dir() {
                continue;
            }
            let name = e.file_name().to_string_lossy().to_lowercase();
            if name.starts_with(&prefix.to_lowercase())
                && name.ends_with(&tail.to_lowercase())
                && name.len() >= prefix.len() + tail.len()
            {
                hits.push(e.path().to_string_lossy().to_string());
            }
        }
    }
    hits.sort();
    hits
}

/// Where the 3DEXPERIENCE Launcher lands. Its presence means this machine is
/// set up for the Connected editions whatever the SOLIDWORKS path looks like.
pub const LAUNCHER_GLOBS: &[&str] = &[
    r"%ProgramFiles%\Dassault Systemes\3DEXPERIENCE Launcher",
    r"%LOCALAPPDATA%\3DEXPERIENCE Launcher",
    r"%ProgramFiles(x86)%\Dassault Systemes\3DEXPERIENCE Launcher",
];

/// Is any of the launcher locations present?
pub fn launcher_present() -> bool {
    LAUNCHER_GLOBS.iter().any(|g| std::path::Path::new(&expand_env(g)).exists())
}

/// One detection pass. Cheap enough to run on every `swx_status`.
pub fn detect() -> Detection {
    #[cfg(windows)]
    {
        windows_impl::detect()
    }
    #[cfg(not(windows))]
    {
        // SOLIDWORKS is Windows-only; on any other host the honest answer is
        // "not installed", and the bridge runs its mock backend.
        //
        // `find_shortcut` is still called: it honours SWX_SOLIDWORKS_SHORTCUT,
        // which is how the Linux suite reaches the PlatformShortcut branch. A
        // branch that only a Windows machine with SOLIDWORKS on it can enter is
        // a branch nothing tests.
        Detection { shortcut: find_shortcut(), ..Detection::default() }
    }
}

/// Known install-path shapes. `*` matches exactly one path component (the
/// year-versioned folder SOLIDWORKS Corp uses).
pub const CANDIDATE_GLOBS: &[&str] = &[
    r"%ProgramFiles%\SOLIDWORKS Corp\SOLIDWORKS\SLDWORKS.exe",
    r"%ProgramFiles%\SOLIDWORKS Corp\*\SOLIDWORKS\SLDWORKS.exe",
    r"%ProgramFiles%\Dassault Systemes\SOLIDWORKS\SLDWORKS.exe",
    // 3DEXPERIENCE SOLIDWORKS Connected installs under the 3DEXPERIENCE launcher:
    r"%ProgramFiles%\Dassault Systemes\SOLIDWORKS 3DEXPERIENCE*\SOLIDWORKS\SLDWORKS.exe",
    r"%ProgramFiles%\Dassault Systemes\*\SOLIDWORKS\SLDWORKS.exe",
];

/// Expand `%VAR%` references; an undefined variable expands to itself so the
/// resulting path simply fails to exist instead of silently matching `\`.
pub fn expand_env(pattern: &str) -> String {
    let mut out = String::new();
    let mut rest = pattern;
    while let Some(start) = rest.find('%') {
        out.push_str(&rest[..start]);
        let after = &rest[start + 1..];
        match after.find('%') {
            Some(end) => {
                let name = &after[..end];
                match std::env::var(name) {
                    Ok(v) => out.push_str(&v),
                    Err(_) => {
                        out.push('%');
                        out.push_str(name);
                        out.push('%');
                    }
                }
                rest = &after[end + 1..];
            }
            None => {
                out.push('%');
                out.push_str(after);
                rest = "";
            }
        }
    }
    out.push_str(rest);
    out
}

/// Resolve one candidate pattern to existing files. Supports at most one `*`
/// component — which is all the SOLIDWORKS layouts need, and keeps a glob crate
/// out of the dependency list.
pub fn expand_glob(pattern: &str) -> Vec<String> {
    let expanded = expand_env(pattern);
    let star = match expanded.find('*') {
        None => {
            return if std::path::Path::new(&expanded).is_file() { vec![expanded] } else { vec![] }
        }
        Some(i) => i,
    };
    // The `*` lives inside ONE path component, which may also carry a literal
    // prefix and/or suffix (e.g. `SOLIDWORKS 3DEXPERIENCE*` matches
    // `SOLIDWORKS 3DEXPERIENCE R2026x`). Split the pattern at the component
    // boundaries around the star, then match directory names by prefix/suffix.
    let head = &expanded[..star];
    let tail = &expanded[star + 1..];
    let sep = |c: char| c == '\\' || c == '/';
    let (parent, name_prefix) = match head.rfind(sep) {
        Some(i) => (&head[..i], &head[i + 1..]),
        None => ("", head),
    };
    let (name_suffix, rest) = match tail.find(sep) {
        Some(i) => (&tail[..i], tail[i + 1..].to_string()),
        None => (tail, String::new()),
    };
    let mut hits = Vec::new();
    if let Ok(entries) = std::fs::read_dir(if parent.is_empty() { "." } else { parent }) {
        for e in entries.flatten() {
            if !e.path().is_dir() {
                continue;
            }
            let name = e.file_name().to_string_lossy().to_string();
            // Case-insensitive: Windows paths are, and the installers are inconsistent.
            let (lname, lpre, lsuf) = (
                name.to_lowercase(),
                name_prefix.to_lowercase(),
                name_suffix.to_lowercase(),
            );
            if !lname.starts_with(&lpre)
                || !lname.ends_with(&lsuf)
                || lname.len() < lpre.len() + lsuf.len()
            {
                continue;
            }
            let candidate = if rest.is_empty() {
                e.path()
            } else {
                e.path().join(rest.replace('\\', std::path::MAIN_SEPARATOR_STR))
            };
            if candidate.is_file() {
                hits.push(candidate.to_string_lossy().to_string());
            }
        }
    }
    hits.sort();
    hits
}

#[cfg(windows)]
mod windows_impl {
    use super::{expand_glob, Detection, CANDIDATE_GLOBS};
    use std::os::windows::process::CommandExt;
    use winreg::enums::{HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE};
    use winreg::RegKey;

    const APP_PATHS: &str =
        r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\SLDWORKS.exe";
    const CREATE_NO_WINDOW: u32 = 0x0800_0000;

    /// The App Paths key — the most reliable signal when it is present.
    fn app_paths_lookup() -> Option<String> {
        for root in [HKEY_LOCAL_MACHINE, HKEY_CURRENT_USER] {
            let hive = RegKey::predef(root);
            if let Ok(key) = hive.open_subkey(APP_PATHS) {
                if let Ok(path) = key.get_value::<String, _>("") {
                    if std::path::Path::new(&path).is_file() {
                        return Some(path);
                    }
                }
            }
        }
        None
    }

    /// `HKLM\SOFTWARE\SolidWorks\SOLIDWORKS <year>` subkeys.
    fn registry_versions() -> Vec<String> {
        let hive = RegKey::predef(HKEY_LOCAL_MACHINE);
        let mut out = Vec::new();
        if let Ok(key) = hive.open_subkey(r"SOFTWARE\SolidWorks") {
            for name in key.enum_keys().flatten() {
                if name.to_uppercase().starts_with("SOLIDWORKS ") {
                    out.push(name);
                }
            }
        }
        out
    }

    fn is_running() -> bool {
        std::process::Command::new("tasklist")
            .args(["/FI", "IMAGENAME eq SLDWORKS.exe", "/FO", "CSV", "/NH"])
            .creation_flags(CREATE_NO_WINDOW)
            .output()
            .map(|o| String::from_utf8_lossy(&o.stdout)
                .to_ascii_lowercase()
                .contains("sldworks.exe"))   // tasklist prints lowercase

            .unwrap_or(false)
    }

    /// A classic install's own folder: `HKLM\SOFTWARE\SolidWorks\SOLIDWORKS
    /// <year>\Setup` value "SolidWorks Folder" (newest year first). UNMEASURED.
    fn classic_setup_exe(versions: &[String]) -> Option<String> {
        let hive = RegKey::predef(HKEY_LOCAL_MACHINE);
        let mut years = super::classic_years_from(None, versions, "unknown");
        years.reverse();
        for y in years {
            if let Ok(key) = hive.open_subkey(format!(r"SOFTWARE\SolidWorks\SOLIDWORKS {y}\Setup")) {
                if let Ok(dir) = key.get_value::<String, _>("SolidWorks Folder") {
                    let exe = std::path::Path::new(dir.trim_end_matches(['\\', '/'])).join("SLDWORKS.exe");
                    if exe.is_file() {
                        return Some(exe.to_string_lossy().to_string());
                    }
                }
            }
        }
        None
    }

    pub fn detect() -> Detection {
        let versions = registry_versions();
        let mut exe = app_paths_lookup().or_else(|| classic_setup_exe(&versions));
        if exe.is_none() {
            for pattern in CANDIDATE_GLOBS {
                let hits = expand_glob(pattern);
                if let Some(last) = hits.last() {
                    exe = Some(last.clone());
                    break;
                }
            }
        }
        Detection {
            installed: exe.is_some(),
            exe,
            registry_versions: versions,
            running: is_running(),
            launcher_present: super::launcher_present(),
            shortcut: super::find_shortcut(),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn the_launch_route_prefers_com_but_falls_back_to_the_platform_shortcut() {
        // THE LAUNCH ANSWER, as a decision table. A positively-identified
        // desktop install attaches over COM; anything else launches through the
        // shortcut the Platform installed, IF there is one; and only the absence
        // of a shortcut blocks.
        let lnk = r"C:\ProgramData\...\SOLIDWORKS Design.lnk";
        assert_eq!(launch_route_from(true, "desktop", None), LaunchRoute::Com);
        assert_eq!(
            launch_route_from(true, "desktop", Some(lnk)),
            LaunchRoute::Com,
            "a desktop install attaches directly rather than running an external process"
        );
        // The case this whole route exists for.
        assert_eq!(
            launch_route_from(true, "connected", Some(lnk)),
            LaunchRoute::PlatformShortcut(lnk.to_string()),
            "SOLIDWORKS Connected CAN be started — through the launcher chain in its shortcut"
        );
        // `installed` is deliberately not required: the exe globs miss the
        // Connected layouts, and a shortcut IS evidence of an install.
        assert_eq!(
            launch_route_from(false, "unknown", Some(lnk)),
            LaunchRoute::PlatformShortcut(lnk.to_string())
        );
        // Blocked ONLY when there is nothing sanctioned to invoke.
        assert_eq!(launch_route_from(true, "connected", None), LaunchRoute::Blocked);
        assert_eq!(launch_route_from(false, "unknown", None), LaunchRoute::Blocked);
        assert_eq!(
            launch_route_from(true, "connected", Some("")),
            LaunchRoute::Blocked,
            "an empty shortcut string is not a shortcut"
        );
    }

    #[test]
    fn the_shortcut_ranking_never_picks_the_uninstaller() {
        // A Start-Menu group holds the application next to an uninstaller and a
        // pile of side tools. "First .lnk found" would run whichever the
        // filesystem happened to list first — on somebody's machine.
        let group = |n: &str| format!(r"C:\ProgramData\MS\Start Menu\Programs\DS SW 3DX\{n}");
        let candidates: Vec<String> = [
            "Uninstall SOLIDWORKS.lnk",
            "SOLIDWORKS Rx 2026.lnk",
            "SOLIDWORKS Tools.lnk",
            "SOLIDWORKS Design.lnk",
            "SOLIDWORKS Visualize.lnk",
        ]
        .iter()
        .map(|n| group(n))
        .collect();
        assert_eq!(
            pick_shortcut(&candidates),
            Some(group("SOLIDWORKS Design.lnk")),
            "the application, not a tool and certainly not the uninstaller"
        );

        // Nothing recognisable: None, so the caller asks the user rather than
        // running something arbitrary.
        let junk: Vec<String> =
            ["Uninstall SOLIDWORKS.lnk", "SOLIDWORKS Rx.lnk", "readme.txt", "Notepad.lnk"]
                .iter()
                .map(|n| group(n))
                .collect();
        assert_eq!(
            pick_shortcut(&junk),
            None,
            "no application shortcut is 'blocked', which asks the user — never a guess"
        );
        // Non-.lnk files in the folder are not launch candidates.
        assert_eq!(pick_shortcut(&[group("SOLIDWORKS Design.exe")]), None);
        // The plain name is accepted when that is what the group calls it.
        assert_eq!(
            pick_shortcut(&[group("SOLIDWORKS.lnk")]),
            Some(group("SOLIDWORKS.lnk"))
        );
    }

    #[test]
    fn the_release_folder_is_found_by_wildcard() {
        // `expand_glob` requires the resolved candidate to be a FILE, so it
        // cannot answer "which release folder does this machine have".
        let tmp = std::env::temp_dir().join(format!("swx-dirglob-{}", std::process::id()));
        let _ = std::fs::remove_dir_all(&tmp);
        std::fs::create_dir_all(tmp.join("Dassault Systemes SOLIDWORKS 3DEXPERIENCE R2026x")).unwrap();
        std::fs::create_dir_all(tmp.join("Something Else")).unwrap();
        let hits = expand_dir_glob(&format!(
            "{}/Dassault Systemes SOLIDWORKS 3DEXPERIENCE*",
            tmp.display()
        ));
        assert_eq!(hits.len(), 1, "matched {hits:?}");
        assert!(hits[0].ends_with("R2026x"));
        assert!(expand_dir_glob(&format!("{}/Nope*", tmp.display())).is_empty());
        let _ = std::fs::remove_dir_all(&tmp);
    }

    #[test]
    fn the_edition_decision_covers_both_real_install_layouts() {
        let none: Vec<String> = vec![];
        // The machine this bridge was built against: SOLIDWORKS Design
        // Professional for Makers 2026, whose registry version string is the
        // UNREMARKABLE "SOLIDWORKS 2026" — the path is what gives it away.
        assert_eq!(
            edition_from(
                Some(r"C:\Program Files\Dassault Systemes\SOLIDWORKS 3DEXPERIENCE R2026x\SOLIDWORKS\SLDWORKS.exe"),
                &["SOLIDWORKS 2026".to_string()],
                false,
            ),
            "connected"
        );
        // Classic desktop layout — the ONLY thing that earns a launch.
        assert_eq!(
            edition_from(
                Some(r"C:\Program Files\SOLIDWORKS Corp\SOLIDWORKS\SLDWORKS.exe"),
                &none,
                false,
            ),
            "desktop"
        );
        // ...and the launcher's presence overrides even that, because a machine
        // with the 3DEXPERIENCE Launcher on it is a Connected machine.
        assert_eq!(
            edition_from(Some(r"C:\Program Files\SOLIDWORKS Corp\SOLIDWORKS\SLDWORKS.exe"), &none, true),
            "connected"
        );
        // A registry entry naming 3DEXPERIENCE is signal on its own.
        assert_eq!(
            edition_from(Some(r"C:\somewhere\SLDWORKS.exe"), &["SOLIDWORKS 3DEXPERIENCE R2026x".to_string()], false),
            "connected"
        );
        // An unrecognised path is UNKNOWN, not "desktop". This is the bit that
        // matters: the previous version answered "desktop" for anything without
        // 3DEXPERIENCE in it, so a layout nobody had seen would have fired the
        // COM launch that produces the refusal modal.
        assert_eq!(edition_from(Some(r"C:\Program Files\Dassault Systemes\SOLIDWORKS\SLDWORKS.exe"), &none, false), "unknown");
        assert_eq!(edition_from(Some(r"D:\weird\SLDWORKS.exe"), &none, false), "unknown");
        // THE CLASSIC REGISTRY (issue #1/#2's SOLIDWORKS 2021): a year key, an exe,
        // no 3DEXPERIENCE anywhere -> desktop; with a 3DEXPERIENCE marker -> connected.
        let y2021 = vec!["SOLIDWORKS 2021".to_string()];
        assert_eq!(edition_from(Some(r"D:\SW2021\SLDWORKS.exe"), &y2021, false), "desktop");
        assert_eq!(edition_from(Some(r"D:\SW2021\SLDWORKS.exe"), &y2021, true), "connected");
        assert_eq!(edition_from(None, &y2021, false), "unknown", "no exe: nothing to launch");
        assert_eq!(registry_years(&["SOLIDWORKS 2021".into(), "SOLIDWORKS 3DEXPERIENCE R2026x".into(), "SOLIDWORKS 2019".into()]), vec![2019, 2021]);
        // classicYears (0.10.1): MEASURED 2026-10-13, the 3DEXPERIENCE 2026 install
        // also writes "SOLIDWORKS 2026" — that year is not a classic install.
        let x2026 = r"C:\Program Files\Dassault Systemes\SOLIDWORKS 3DEXPERIENCE R2026x\SOLIDWORKS\SLDWORKS.exe";
        let reg26 = vec!["SOLIDWORKS 2026".to_string()];
        assert_eq!(edition_from(Some(x2026), &reg26, true), "connected", "the edition decision is unchanged");
        assert_eq!(registry_years(&reg26), vec![2026]);
        assert!(classic_years_from(Some(x2026), &reg26, "connected").is_empty(), "the Connected release is not classic");
        assert!(classic_years_from(None, &reg26, "connected").is_empty(), "a marker naming no year: no year can be shown to be classic");
        let mixed = vec!["SOLIDWORKS 2021".to_string(), "SOLIDWORKS 2026".to_string(), "SOLIDWORKS 3DEXPERIENCE R2026x".to_string()];
        assert_eq!(classic_years_from(None, &mixed, "connected"), vec![2021], "a classic 2021 beside a Connected 2026");
        assert_eq!(classic_years_from(Some(r"D:\SW2021\SLDWORKS.exe"), &y2021, "desktop"), vec![2021]);
        assert_eq!(edition_from(None, &none, false), "unknown");
    }

    #[test]
    fn only_a_positively_identified_desktop_install_may_be_launched() {
        let d = |exe: Option<&str>, launcher: bool| Detection {
            installed: exe.is_some(),
            shortcut: None,
            exe: exe.map(String::from),
            registry_versions: vec![],
            running: false,
            launcher_present: launcher,
        };
        assert!(d(Some(r"C:\Program Files\SOLIDWORKS Corp\SOLIDWORKS\SLDWORKS.exe"), false).can_launch());
        // Connected: never.
        assert!(!d(Some(r"C:\Program Files\Dassault Systemes\SOLIDWORKS 3DEXPERIENCE R2026x\SOLIDWORKS\SLDWORKS.exe"), false).can_launch());
        // Unknown: never. Refusing a launch we could have made costs one
        // sentence; making one we should have refused costs the user a modal
        // dialog and a dead process.
        assert!(!d(Some(r"D:\weird\SLDWORKS.exe"), false).can_launch());
        // Not installed at all: never.
        assert!(!d(None, false).can_launch());
        // ...and the JSON a caller branches on carries both facts.
        let j = d(Some(r"C:\Program Files\SOLIDWORKS Corp\SOLIDWORKS\SLDWORKS.exe"), false).to_json();
        assert_eq!(j["edition"], "desktop");
        assert_eq!(j["canLaunchProgrammatically"], true);
    }

    #[test]
    fn env_expansion_keeps_unknown_vars_literal() {
        // No set_var here: mutating the process environment races every other
        // test thread (get_var/set_var is not thread-safe), which is UB. Read a
        // variable cargo always sets for the test process instead.
        let known = "CARGO_PKG_NAME";
        let expected = std::env::var(known).unwrap();
        assert_eq!(expand_env(&format!("%{known}%/x.exe")), format!("{expected}/x.exe"));
        assert_eq!(expand_env(r"%NO_SUCH_VAR_XYZ%\x"), r"%NO_SUCH_VAR_XYZ%\x");
        assert_eq!(expand_env("plain"), "plain");
        assert_eq!(expand_env("no percent at all"), "no percent at all");
    }

    #[test]
    fn glob_matches_one_wildcard_component() {
        let base = std::env::temp_dir().join(format!("swx-glob-{}", std::process::id()));
        let deep = base.join("2026").join("SOLIDWORKS");
        std::fs::create_dir_all(&deep).unwrap();
        let exe = deep.join("SLDWORKS.exe");
        std::fs::write(&exe, b"x").unwrap();
        let pattern = format!("{}/*/SOLIDWORKS/SLDWORKS.exe", base.display());
        let hits = expand_glob(&pattern);
        assert_eq!(hits.len(), 1, "{hits:?}");
        assert!(hits[0].contains("SLDWORKS.exe"));
        // an exact path that does not exist yields nothing
        assert!(expand_glob(&format!("{}/nope.exe", base.display())).is_empty());
        let _ = std::fs::remove_dir_all(&base);
    }

    #[test]
    fn non_windows_detection_is_honest() {
        let d = detect();
        if !cfg!(windows) {
            assert!(!d.installed);
            assert_eq!(d.to_json()["platform"], std::env::consts::OS);
        }
    }
}

#[cfg(test)]
mod live_regression_tests {
    use super::expand_glob;

    /// A wildcard INSIDE a component (the real 3DEXPERIENCE layout:
    /// `SOLIDWORKS 3DEXPERIENCE R2026x`). This is what failed on Drew's live
    /// install: the old splitter looked for a dir literally named
    /// `SOLIDWORKS 3DEXPERIENCE`.
    #[test]
    fn partial_component_wildcard_matches_versioned_dir() {
        let tmp = std::env::temp_dir().join(format!("swxglob{}", std::process::id()));
        let dir = tmp.join("SOLIDWORKS 3DEXPERIENCE R2026x").join("SOLIDWORKS");
        std::fs::create_dir_all(&dir).unwrap();
        let exe = dir.join("SLDWORKS.exe");
        std::fs::write(&exe, b"x").unwrap();
        let pat = format!("{}/SOLIDWORKS 3DEXPERIENCE*/SOLIDWORKS/SLDWORKS.exe", tmp.display());
        let hits = expand_glob(&pat);
        assert_eq!(hits.len(), 1, "expected the versioned dir to match: {hits:?}");
        assert!(hits[0].to_lowercase().contains("r2026x"));
        // A prefix that does not match must not match anything.
        assert!(expand_glob(&format!("{}/NOPE*/SOLIDWORKS/SLDWORKS.exe", tmp.display())).is_empty());
        std::fs::remove_dir_all(&tmp).ok();
    }
}