use crate::Error;
use chrono::NaiveDate;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use windows_registry::{CURRENT_USER, Key, LOCAL_MACHINE};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RegistrySource {
LocalMachine64,
LocalMachine32,
CurrentUser,
}
impl std::fmt::Display for RegistrySource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RegistrySource::LocalMachine64 => write!(f, "HKLM\\64-bit"),
RegistrySource::LocalMachine32 => write!(f, "HKLM\\32-bit"),
RegistrySource::CurrentUser => write!(f, "HKCU"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Software {
pub name: String,
pub version: Option<String>,
pub publisher: Option<String>,
pub install_date: Option<NaiveDate>,
pub install_location: Option<PathBuf>,
pub source: RegistrySource,
}
pub struct SoftwareScanner {
include_user_installs: bool,
include_32bit: bool,
}
impl Default for SoftwareScanner {
fn default() -> Self {
Self::new()
}
}
impl SoftwareScanner {
pub fn new() -> Self {
SoftwareScanner {
include_user_installs: true,
include_32bit: true,
}
}
pub fn include_user_installs(mut self, include: bool) -> Self {
self.include_user_installs = include;
self
}
pub fn include_32bit(mut self, include: bool) -> Self {
self.include_32bit = include;
self
}
pub fn scan(&self) -> Result<Vec<Software>, Error> {
let mut result = Vec::new();
if let Ok(software) = self.scan_key(
LOCAL_MACHINE,
r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall",
RegistrySource::LocalMachine64,
) {
result.extend(software);
}
if self.include_32bit {
if let Ok(software) = self.scan_key(
LOCAL_MACHINE,
r"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall",
RegistrySource::LocalMachine32,
) {
result.extend(software);
}
}
if self.include_user_installs {
if let Ok(software) = self.scan_key(
CURRENT_USER,
r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall",
RegistrySource::CurrentUser,
) {
result.extend(software);
}
}
result.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
Ok(result)
}
fn scan_key(
&self,
root: &Key,
path: &str,
source: RegistrySource,
) -> Result<Vec<Software>, Error> {
let key = root.open(path)?;
let mut result = Vec::new();
for subkey_name in key.keys()? {
if let Ok(subkey) = key.open(&subkey_name) {
if let Some(software) = self.parse_software_key(&subkey, source) {
result.push(software);
}
}
}
Ok(result)
}
fn parse_software_key(&self, key: &Key, source: RegistrySource) -> Option<Software> {
let name = key.get_string("DisplayName").ok()?;
if name.trim().is_empty() {
return None;
}
let version = key.get_string("DisplayVersion").ok();
let publisher = key.get_string("Publisher").ok();
let install_location = key
.get_string("InstallLocation")
.ok()
.filter(|s| !s.is_empty())
.map(PathBuf::from);
let install_date = key
.get_string("InstallDate")
.ok()
.and_then(|s| parse_install_date(&s));
Some(Software {
name,
version,
publisher,
install_date,
install_location,
source,
})
}
}
fn parse_install_date(s: &str) -> Option<NaiveDate> {
if s.len() != 8 {
return None;
}
let year: i32 = s[0..4].parse().ok()?;
let month: u32 = s[4..6].parse().ok()?;
let day: u32 = s[6..8].parse().ok()?;
NaiveDate::from_ymd_opt(year, month, day)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_install_date_valid() {
assert_eq!(
parse_install_date("20240115"),
NaiveDate::from_ymd_opt(2024, 1, 15)
);
}
#[test]
fn test_parse_install_date_invalid() {
assert_eq!(parse_install_date("invalid"), None);
assert_eq!(parse_install_date("2024"), None);
assert_eq!(parse_install_date(""), None);
assert_eq!(parse_install_date("20240230"), None); assert_eq!(parse_install_date("20241301"), None); assert_eq!(parse_install_date("ABCDEFGH"), None); }
#[test]
fn test_parse_install_date_future() {
assert_eq!(
parse_install_date("99991231"),
NaiveDate::from_ymd_opt(9999, 12, 31)
);
}
}