Skip to main content

winreg_artifacts/
lxss.rs

1//! WSL distro registration parser — HKCU\Software\Microsoft\Windows\CurrentVersion\Lxss
2
3use std::io::Cursor;
4use std::path::PathBuf;
5
6use winreg_core::hive::Hive;
7
8// ── Types ─────────────────────────────────────────────────────────────────────
9
10#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
11pub enum DistroVersion {
12    Wsl1,
13    Wsl2,
14    Unknown,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
18pub enum DistroState {
19    Installed,
20    Running,
21    Unknown,
22}
23
24#[derive(Debug, Clone, serde::Serialize)]
25pub struct LxssDistro {
26    pub guid: String,
27    pub distribution_name: String,
28    pub package_family_name: Option<String>,
29    pub base_path: String,
30    pub state: DistroState,
31    pub version: DistroVersion,
32    pub default_uid: Option<u32>,
33    pub is_default: bool,
34}
35
36impl LxssDistro {
37    /// Returns the path to `ext4.vhdx` for WSL2 distros; `None` for WSL1.
38    ///
39    /// WSL2 stores the Linux filesystem at `<BasePath>\ext4.vhdx`.
40    /// WSL1 uses a directory tree under `%LOCALAPPDATA%\lxss\` instead.
41    pub fn vhdx_path(&self) -> Option<PathBuf> {
42        if self.version != DistroVersion::Wsl2 {
43            return None;
44        }
45        let mut path = PathBuf::from(&self.base_path);
46        path.push("ext4.vhdx");
47        Some(path)
48    }
49}
50
51// ── Key paths ─────────────────────────────────────────────────────────────────
52
53const LXSS_PATH: &str = "Software\\Microsoft\\Windows\\CurrentVersion\\Lxss";
54
55// ── Helpers ───────────────────────────────────────────────────────────────────
56
57/// Returns true for GUID-format names like `{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}`.
58fn is_guid(name: &str) -> bool {
59    let s = name.trim();
60    if s.len() != 38 {
61        return false;
62    }
63    let b = s.as_bytes();
64    b[0] == b'{'
65        && b[37] == b'}'
66        && b[9] == b'-'
67        && b[14] == b'-'
68        && b[19] == b'-'
69        && b[24] == b'-'
70        && b[1..9].iter().all(|c| c.is_ascii_hexdigit())
71        && b[10..14].iter().all(|c| c.is_ascii_hexdigit())
72        && b[15..19].iter().all(|c| c.is_ascii_hexdigit())
73        && b[20..24].iter().all(|c| c.is_ascii_hexdigit())
74        && b[25..37].iter().all(|c| c.is_ascii_hexdigit())
75}
76
77fn str_val(key: &winreg_core::key::Key<'_>, name: &str) -> Option<String> {
78    key.value(name)
79        .ok()
80        .flatten()
81        .and_then(|v| v.as_string().ok())
82}
83
84fn u32_val(key: &winreg_core::key::Key<'_>, name: &str) -> Option<u32> {
85    key.value(name).ok().flatten().and_then(|v| v.as_u32().ok())
86}
87
88// ── Parser ────────────────────────────────────────────────────────────────────
89
90/// Parse WSL distro registrations from an NTUSER.DAT hive.
91pub fn parse(hive: &Hive<Cursor<Vec<u8>>>) -> Vec<LxssDistro> {
92    let lxss_key = match hive.open_key(LXSS_PATH) {
93        Ok(Some(k)) => k,
94        _ => return Vec::new(),
95    };
96
97    let default_guid = str_val(&lxss_key, "DefaultDistribution").unwrap_or_default();
98
99    let subkeys = match lxss_key.subkeys() {
100        Ok(s) => s,
101        Err(_) => return Vec::new(),
102    };
103
104    let mut distros = Vec::new();
105
106    for subkey in subkeys {
107        let guid = subkey.name();
108        if !is_guid(&guid) {
109            continue;
110        }
111
112        let distribution_name = match str_val(&subkey, "DistributionName") {
113            Some(n) => n,
114            None => continue,
115        };
116
117        let base_path = match str_val(&subkey, "BasePath") {
118            Some(p) => p,
119            None => continue,
120        };
121
122        let package_family_name = str_val(&subkey, "PackageFamilyName");
123
124        let state = match u32_val(&subkey, "State") {
125            Some(1) => DistroState::Installed,
126            Some(4) => DistroState::Running,
127            _ => DistroState::Unknown,
128        };
129
130        let version = match u32_val(&subkey, "Version") {
131            Some(1) => DistroVersion::Wsl1,
132            Some(2) => DistroVersion::Wsl2,
133            _ => DistroVersion::Unknown,
134        };
135
136        let default_uid = u32_val(&subkey, "DefaultUid");
137        let is_default = !default_guid.is_empty() && guid == default_guid;
138
139        distros.push(LxssDistro {
140            guid,
141            distribution_name,
142            package_family_name,
143            base_path,
144            state,
145            version,
146            default_uid,
147            is_default,
148        });
149    }
150
151    distros
152}