From a5324cad8a2688f99a42b4a0d8abafaeba8ccb50 Mon Sep 17 00:00:00 2001 From: Russell Banks <74878137+russellbanks@users.noreply.github.com> Date: Thu, 30 Jan 2025 15:48:47 +0000 Subject: [PATCH] Get Product Code from NSIS installers --- src/installers/nsis/entry/mod.rs | 35 +++++++++++++++------ src/installers/nsis/mod.rs | 30 ++++++++---------- src/installers/nsis/registry.rs | 52 ++++++++++++++++++++++++++++++++ src/installers/nsis/state.rs | 9 ++++-- src/installers/utils/registry.rs | 4 ++- 5 files changed, 98 insertions(+), 32 deletions(-) create mode 100644 src/installers/nsis/registry.rs diff --git a/src/installers/nsis/entry/mod.rs b/src/installers/nsis/entry/mod.rs index 9996892e..631375d0 100644 --- a/src/installers/nsis/entry/mod.rs +++ b/src/installers/nsis/entry/mod.rs @@ -242,6 +242,7 @@ pub enum Entry { ini_file: I32, } = 49u32.to_le(), DeleteReg { + reserved: I32, root: RegRoot, key_name: I32, value_name: I32, @@ -323,28 +324,28 @@ pub enum Entry { impl Entry { #[expect(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] - pub fn update_vars(&self, state: &mut NsisState) { + pub fn execute(&self, state: &mut NsisState) { match self { Self::GetFullPathname { output, input } => { - state.user_variables.insert( + state.variables.insert( output.get().unsigned_abs() as usize, state.get_string(input.get()), ); } Self::SearchPath { output, filename } => { - state.user_variables.insert( + state.variables.insert( output.get().unsigned_abs() as usize, state.get_string(filename.get()), ); } Self::GetTempFilename { output, base_dir } => { - state.user_variables.insert( + state.variables.insert( output.get().unsigned_abs() as usize, state.get_string(base_dir.get()), ); } Self::StrLen { output, input } => { - state.user_variables.insert( + state.variables.insert( output.get().unsigned_abs() as usize, Cow::Owned(state.get_string(input.get()).len().to_string()), ); @@ -370,7 +371,7 @@ impl Entry { let start = u32::try_from(start).unwrap_or_default(); if start < result.len() as u32 { - state.user_variables.insert( + state.variables.insert( variable.get().unsigned_abs() as usize, match result { Cow::Borrowed(borrowed) => { @@ -385,7 +386,7 @@ impl Entry { } } else { state - .user_variables + .variables .remove(&(variable.get().unsigned_abs() as usize)); } } @@ -394,7 +395,7 @@ impl Entry { string_with_env_variables, .. } => { - state.user_variables.insert( + state.variables.insert( output.get().unsigned_abs() as usize, state.get_string(string_with_env_variables.get()), ); @@ -422,7 +423,7 @@ impl Entry { 13 => ((input1.get() as u32).wrapping_shr(input2.get() as u32)) as i32, _ => input1.get(), }; - state.user_variables.insert( + state.variables.insert( output.get().unsigned_abs() as usize, Cow::Owned(result.to_string()), ); @@ -440,13 +441,27 @@ impl Entry { } else if *push_pop == PushPop::Pop { if let Some(variable) = state.stack.pop() { state - .user_variables + .variables .insert(variable_or_string.get().unsigned_abs() as usize, variable); } } else if *push_pop == PushPop::Push { state.stack.push(state.get_string(variable_or_string.get())); } } + Self::WriteReg { + root, + key_name, + value_name, + value, + .. + } => { + state.registry.set_value( + *root, + state.get_string(key_name.get()), + state.get_string(value_name.get()), + state.get_string(value.get()), + ); + } _ => {} } } diff --git a/src/installers/nsis/mod.rs b/src/installers/nsis/mod.rs index 386a2ee3..2d5c4258 100644 --- a/src/installers/nsis/mod.rs +++ b/src/installers/nsis/mod.rs @@ -2,6 +2,7 @@ mod entry; mod first_header; mod header; mod language; +mod registry; mod state; mod strings; mod version; @@ -102,23 +103,9 @@ impl Nsis { let mut architecture = Option::from(architecture).filter(|&architecture| architecture != Architecture::X86); - let mut display_name = None; - let mut display_version = None; - let mut display_publisher = None; for entry in entries { - entry.update_vars(&mut state); - if let Entry::WriteReg { - value_name, value, .. - } = entry - { - let value = state.get_string(value.get()); - match &*state.get_string(value_name.get()) { - "DisplayName" => display_name = Some(value), - "DisplayVersion" => display_version = Some(value), - "Publisher" => display_publisher = Some(value), - _ => {} - } - } else if let Entry::ExtractFile { name, .. } = entry { + entry.execute(&mut state); + if let Entry::ExtractFile { name, .. } = entry { let name = state.get_string(name.get()); let file_stem = Utf8Path::new(&name).file_stem(); // If there is an app-64 file, the app is x64. @@ -214,6 +201,11 @@ impl Nsis { .map(Architecture::from_machine) }); + let display_name = state.registry.remove_value("DisplayName"); + let display_version = state.registry.remove_value("DisplayVersion"); + let publisher = state.registry.remove_value("Publisher"); + let product_code = state.registry.get_product_code(); + Ok(Self { installer: Installer { locale: Language::from_code(state.language_table.id.get()) @@ -223,14 +215,16 @@ impl Nsis { architecture: architecture.unwrap_or(Architecture::X86), r#type: Some(InstallerType::Nullsoft), scope: install_dir.as_deref().and_then(Scope::from_install_dir), - apps_and_features_entries: [&display_name, &display_version, &display_publisher] + product_code: product_code.map(str::to_owned), + apps_and_features_entries: [&display_name, &display_version, &publisher] .iter() .any(|option| option.is_some()) .then(|| { vec![AppsAndFeaturesEntry { display_name: display_name.map(Cow::into_owned), - publisher: display_publisher.map(Cow::into_owned), + publisher: publisher.map(Cow::into_owned), display_version: display_version.as_deref().map(Version::new), + product_code: product_code.map(str::to_owned), ..AppsAndFeaturesEntry::default() }] }), diff --git a/src/installers/nsis/registry.rs b/src/installers/nsis/registry.rs new file mode 100644 index 00000000..91ec17b7 --- /dev/null +++ b/src/installers/nsis/registry.rs @@ -0,0 +1,52 @@ +use crate::installers::utils::registry::RegRoot; +use std::borrow::Cow; +use std::collections::HashMap; + +type Values<'data> = HashMap, Cow<'data, str>>; + +type Keys<'data> = HashMap, Values<'data>>; + +// Registry root -< Key name -< Value name - Value +#[derive(Debug)] +pub struct Registry<'data>(HashMap>); + +const CURRENT_VERSION_UNINSTALL: &str = r"Software\Microsoft\Windows\CurrentVersion\Uninstall"; + +impl<'data> Registry<'data> { + pub fn new() -> Self { + Self(HashMap::new()) + } + + pub fn get_product_code(&self) -> Option<&str> { + // Find the first Software\Microsoft\Windows\CurrentVersion\Uninstall\{PRODUCT_CODE} key + // under any root and extract the product code from it + self.0.values().find_map(|keys| { + keys.keys().find_map(|key| { + key.rsplit_once('\\').and_then(|(parent, product_code)| { + (parent == CURRENT_VERSION_UNINSTALL).then_some(product_code) + }) + }) + }) + } + + pub fn set_value( + &mut self, + root: RegRoot, + key: Cow<'data, str>, + name: Cow<'data, str>, + value: Cow<'data, str>, + ) { + self.0 + .entry(root) + .or_default() + .entry(key) + .or_default() + .insert(name, value); + } + + pub fn remove_value(&mut self, name: &str) -> Option> { + self.0 + .values_mut() + .find_map(|keys| keys.values_mut().find_map(|values| values.remove(name))) + } +} diff --git a/src/installers/nsis/state.rs b/src/installers/nsis/state.rs index 6b423aed..3b9b7585 100644 --- a/src/installers/nsis/state.rs +++ b/src/installers/nsis/state.rs @@ -1,6 +1,7 @@ use crate::installers::nsis::header::block::{BlockHeaders, BlockType}; use crate::installers::nsis::header::Header; use crate::installers::nsis::language::table::LanguageTable; +use crate::installers::nsis::registry::Registry; use crate::installers::nsis::strings::code::NsCode; use crate::installers::nsis::strings::shell::Shell; use crate::installers::nsis::strings::var::NsVar; @@ -17,7 +18,8 @@ pub struct NsisState<'data> { pub str_block: &'data [u8], pub language_table: &'data LanguageTable, pub stack: Vec>, - pub user_variables: HashMap>, + pub variables: HashMap>, + pub registry: Registry<'data>, pub version: NsisVersion, } @@ -32,7 +34,8 @@ impl<'data> NsisState<'data> { str_block: BlockType::Strings.get(data, blocks), language_table: LanguageTable::get_main(data, header, blocks)?, stack: Vec::new(), - user_variables: HashMap::new(), + variables: HashMap::new(), + registry: Registry::new(), version: NsisVersion::default(), }; @@ -123,7 +126,7 @@ impl<'data> NsisState<'data> { } else { let index = usize::from(decode_number_from_char(special_char)); if current == u16::from(NsCode::Var.get(self.version)) { - NsVar::resolve(&mut buf, index, &self.user_variables, self.version); + NsVar::resolve(&mut buf, index, &self.variables, self.version); } else if current == u16::from(NsCode::Lang.get(self.version)) { buf.push_str( &self.get_string(self.language_table.string_offsets[index].get()), diff --git a/src/installers/utils/registry.rs b/src/installers/utils/registry.rs index 30f5d292..d01f3884 100644 --- a/src/installers/utils/registry.rs +++ b/src/installers/utils/registry.rs @@ -1,7 +1,9 @@ use zerocopy::{Immutable, KnownLayout, TryFromBytes}; #[expect(dead_code)] -#[derive(Debug, Default, PartialEq, Eq, TryFromBytes, KnownLayout, Immutable)] +#[derive( + Copy, Clone, Debug, Default, Hash, PartialEq, Eq, TryFromBytes, KnownLayout, Immutable, +)] #[repr(u32)] pub enum RegRoot { #[default]