Skip to content

Commit a5324ca

Browse files
committed
Get Product Code from NSIS installers
1 parent dfb4594 commit a5324ca

File tree

5 files changed

+98
-32
lines changed

5 files changed

+98
-32
lines changed

src/installers/nsis/entry/mod.rs

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ pub enum Entry {
242242
ini_file: I32,
243243
} = 49u32.to_le(),
244244
DeleteReg {
245+
reserved: I32,
245246
root: RegRoot,
246247
key_name: I32,
247248
value_name: I32,
@@ -323,28 +324,28 @@ pub enum Entry {
323324

324325
impl Entry {
325326
#[expect(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
326-
pub fn update_vars(&self, state: &mut NsisState) {
327+
pub fn execute(&self, state: &mut NsisState) {
327328
match self {
328329
Self::GetFullPathname { output, input } => {
329-
state.user_variables.insert(
330+
state.variables.insert(
330331
output.get().unsigned_abs() as usize,
331332
state.get_string(input.get()),
332333
);
333334
}
334335
Self::SearchPath { output, filename } => {
335-
state.user_variables.insert(
336+
state.variables.insert(
336337
output.get().unsigned_abs() as usize,
337338
state.get_string(filename.get()),
338339
);
339340
}
340341
Self::GetTempFilename { output, base_dir } => {
341-
state.user_variables.insert(
342+
state.variables.insert(
342343
output.get().unsigned_abs() as usize,
343344
state.get_string(base_dir.get()),
344345
);
345346
}
346347
Self::StrLen { output, input } => {
347-
state.user_variables.insert(
348+
state.variables.insert(
348349
output.get().unsigned_abs() as usize,
349350
Cow::Owned(state.get_string(input.get()).len().to_string()),
350351
);
@@ -370,7 +371,7 @@ impl Entry {
370371

371372
let start = u32::try_from(start).unwrap_or_default();
372373
if start < result.len() as u32 {
373-
state.user_variables.insert(
374+
state.variables.insert(
374375
variable.get().unsigned_abs() as usize,
375376
match result {
376377
Cow::Borrowed(borrowed) => {
@@ -385,7 +386,7 @@ impl Entry {
385386
}
386387
} else {
387388
state
388-
.user_variables
389+
.variables
389390
.remove(&(variable.get().unsigned_abs() as usize));
390391
}
391392
}
@@ -394,7 +395,7 @@ impl Entry {
394395
string_with_env_variables,
395396
..
396397
} => {
397-
state.user_variables.insert(
398+
state.variables.insert(
398399
output.get().unsigned_abs() as usize,
399400
state.get_string(string_with_env_variables.get()),
400401
);
@@ -422,7 +423,7 @@ impl Entry {
422423
13 => ((input1.get() as u32).wrapping_shr(input2.get() as u32)) as i32,
423424
_ => input1.get(),
424425
};
425-
state.user_variables.insert(
426+
state.variables.insert(
426427
output.get().unsigned_abs() as usize,
427428
Cow::Owned(result.to_string()),
428429
);
@@ -440,13 +441,27 @@ impl Entry {
440441
} else if *push_pop == PushPop::Pop {
441442
if let Some(variable) = state.stack.pop() {
442443
state
443-
.user_variables
444+
.variables
444445
.insert(variable_or_string.get().unsigned_abs() as usize, variable);
445446
}
446447
} else if *push_pop == PushPop::Push {
447448
state.stack.push(state.get_string(variable_or_string.get()));
448449
}
449450
}
451+
Self::WriteReg {
452+
root,
453+
key_name,
454+
value_name,
455+
value,
456+
..
457+
} => {
458+
state.registry.set_value(
459+
*root,
460+
state.get_string(key_name.get()),
461+
state.get_string(value_name.get()),
462+
state.get_string(value.get()),
463+
);
464+
}
450465
_ => {}
451466
}
452467
}

src/installers/nsis/mod.rs

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ mod entry;
22
mod first_header;
33
mod header;
44
mod language;
5+
mod registry;
56
mod state;
67
mod strings;
78
mod version;
@@ -102,23 +103,9 @@ impl Nsis {
102103
let mut architecture =
103104
Option::from(architecture).filter(|&architecture| architecture != Architecture::X86);
104105

105-
let mut display_name = None;
106-
let mut display_version = None;
107-
let mut display_publisher = None;
108106
for entry in entries {
109-
entry.update_vars(&mut state);
110-
if let Entry::WriteReg {
111-
value_name, value, ..
112-
} = entry
113-
{
114-
let value = state.get_string(value.get());
115-
match &*state.get_string(value_name.get()) {
116-
"DisplayName" => display_name = Some(value),
117-
"DisplayVersion" => display_version = Some(value),
118-
"Publisher" => display_publisher = Some(value),
119-
_ => {}
120-
}
121-
} else if let Entry::ExtractFile { name, .. } = entry {
107+
entry.execute(&mut state);
108+
if let Entry::ExtractFile { name, .. } = entry {
122109
let name = state.get_string(name.get());
123110
let file_stem = Utf8Path::new(&name).file_stem();
124111
// If there is an app-64 file, the app is x64.
@@ -214,6 +201,11 @@ impl Nsis {
214201
.map(Architecture::from_machine)
215202
});
216203

204+
let display_name = state.registry.remove_value("DisplayName");
205+
let display_version = state.registry.remove_value("DisplayVersion");
206+
let publisher = state.registry.remove_value("Publisher");
207+
let product_code = state.registry.get_product_code();
208+
217209
Ok(Self {
218210
installer: Installer {
219211
locale: Language::from_code(state.language_table.id.get())
@@ -223,14 +215,16 @@ impl Nsis {
223215
architecture: architecture.unwrap_or(Architecture::X86),
224216
r#type: Some(InstallerType::Nullsoft),
225217
scope: install_dir.as_deref().and_then(Scope::from_install_dir),
226-
apps_and_features_entries: [&display_name, &display_version, &display_publisher]
218+
product_code: product_code.map(str::to_owned),
219+
apps_and_features_entries: [&display_name, &display_version, &publisher]
227220
.iter()
228221
.any(|option| option.is_some())
229222
.then(|| {
230223
vec![AppsAndFeaturesEntry {
231224
display_name: display_name.map(Cow::into_owned),
232-
publisher: display_publisher.map(Cow::into_owned),
225+
publisher: publisher.map(Cow::into_owned),
233226
display_version: display_version.as_deref().map(Version::new),
227+
product_code: product_code.map(str::to_owned),
234228
..AppsAndFeaturesEntry::default()
235229
}]
236230
}),

src/installers/nsis/registry.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
use crate::installers::utils::registry::RegRoot;
2+
use std::borrow::Cow;
3+
use std::collections::HashMap;
4+
5+
type Values<'data> = HashMap<Cow<'data, str>, Cow<'data, str>>;
6+
7+
type Keys<'data> = HashMap<Cow<'data, str>, Values<'data>>;
8+
9+
// Registry root -< Key name -< Value name - Value
10+
#[derive(Debug)]
11+
pub struct Registry<'data>(HashMap<RegRoot, Keys<'data>>);
12+
13+
const CURRENT_VERSION_UNINSTALL: &str = r"Software\Microsoft\Windows\CurrentVersion\Uninstall";
14+
15+
impl<'data> Registry<'data> {
16+
pub fn new() -> Self {
17+
Self(HashMap::new())
18+
}
19+
20+
pub fn get_product_code(&self) -> Option<&str> {
21+
// Find the first Software\Microsoft\Windows\CurrentVersion\Uninstall\{PRODUCT_CODE} key
22+
// under any root and extract the product code from it
23+
self.0.values().find_map(|keys| {
24+
keys.keys().find_map(|key| {
25+
key.rsplit_once('\\').and_then(|(parent, product_code)| {
26+
(parent == CURRENT_VERSION_UNINSTALL).then_some(product_code)
27+
})
28+
})
29+
})
30+
}
31+
32+
pub fn set_value(
33+
&mut self,
34+
root: RegRoot,
35+
key: Cow<'data, str>,
36+
name: Cow<'data, str>,
37+
value: Cow<'data, str>,
38+
) {
39+
self.0
40+
.entry(root)
41+
.or_default()
42+
.entry(key)
43+
.or_default()
44+
.insert(name, value);
45+
}
46+
47+
pub fn remove_value(&mut self, name: &str) -> Option<Cow<'data, str>> {
48+
self.0
49+
.values_mut()
50+
.find_map(|keys| keys.values_mut().find_map(|values| values.remove(name)))
51+
}
52+
}

src/installers/nsis/state.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::installers::nsis::header::block::{BlockHeaders, BlockType};
22
use crate::installers::nsis::header::Header;
33
use crate::installers::nsis::language::table::LanguageTable;
4+
use crate::installers::nsis::registry::Registry;
45
use crate::installers::nsis::strings::code::NsCode;
56
use crate::installers::nsis::strings::shell::Shell;
67
use crate::installers::nsis::strings::var::NsVar;
@@ -17,7 +18,8 @@ pub struct NsisState<'data> {
1718
pub str_block: &'data [u8],
1819
pub language_table: &'data LanguageTable,
1920
pub stack: Vec<Cow<'data, str>>,
20-
pub user_variables: HashMap<usize, Cow<'data, str>>,
21+
pub variables: HashMap<usize, Cow<'data, str>>,
22+
pub registry: Registry<'data>,
2123
pub version: NsisVersion,
2224
}
2325

@@ -32,7 +34,8 @@ impl<'data> NsisState<'data> {
3234
str_block: BlockType::Strings.get(data, blocks),
3335
language_table: LanguageTable::get_main(data, header, blocks)?,
3436
stack: Vec::new(),
35-
user_variables: HashMap::new(),
37+
variables: HashMap::new(),
38+
registry: Registry::new(),
3639
version: NsisVersion::default(),
3740
};
3841

@@ -123,7 +126,7 @@ impl<'data> NsisState<'data> {
123126
} else {
124127
let index = usize::from(decode_number_from_char(special_char));
125128
if current == u16::from(NsCode::Var.get(self.version)) {
126-
NsVar::resolve(&mut buf, index, &self.user_variables, self.version);
129+
NsVar::resolve(&mut buf, index, &self.variables, self.version);
127130
} else if current == u16::from(NsCode::Lang.get(self.version)) {
128131
buf.push_str(
129132
&self.get_string(self.language_table.string_offsets[index].get()),

src/installers/utils/registry.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
use zerocopy::{Immutable, KnownLayout, TryFromBytes};
22

33
#[expect(dead_code)]
4-
#[derive(Debug, Default, PartialEq, Eq, TryFromBytes, KnownLayout, Immutable)]
4+
#[derive(
5+
Copy, Clone, Debug, Default, Hash, PartialEq, Eq, TryFromBytes, KnownLayout, Immutable,
6+
)]
57
#[repr(u32)]
68
pub enum RegRoot {
79
#[default]

0 commit comments

Comments
 (0)