use std::collections::BTreeMap;
use std::io::Cursor;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error {
#[error("MSI open: {0}")]
Open(std::io::Error),
#[error("MSI schema: {0}")]
Schema(String),
#[error("unresolved property: [{0}]")]
UnresolvedProperty(String),
}
#[derive(Debug, Clone)]
pub enum InstallAction {
CreateDirectory {
id: String,
path: String,
},
WriteFile {
id: String,
path: String,
size: u64,
component: String,
bytes: Option<Vec<u8>>,
},
RegSet {
id: String,
hive: RegHive,
key: String,
name: String,
value: RegValue,
component: String,
},
SnapshotProperties(BTreeMap<String, String>),
Log(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RegHive {
ClassesRoot,
CurrentUser,
LocalMachine,
Users,
}
impl RegHive {
#[must_use]
pub fn short(self) -> &'static str {
match self {
RegHive::ClassesRoot => "HKCR",
RegHive::CurrentUser => "HKCU",
RegHive::LocalMachine => "HKLM",
RegHive::Users => "HKU",
}
}
#[must_use]
pub fn from_msi_root(root: i32) -> RegHive {
match root {
0 => RegHive::ClassesRoot,
1 => RegHive::CurrentUser,
3 => RegHive::Users,
_ => RegHive::LocalMachine,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RegValue {
Empty,
Sz(String),
ExpandSz(String),
MultiSz(Vec<String>),
Dword(u32),
Binary(Vec<u8>),
}
pub trait InstallSink {
fn emit(&mut self, action: InstallAction) -> bool;
fn override_property(&mut self, _name: &str) -> Option<String> {
None
}
fn install_component(&mut self, _component: &str) -> bool {
true
}
}
#[derive(Debug, Default, Clone)]
pub struct InstallOptions {
pub properties: BTreeMap<String, String>,
pub strict_property_refs: bool,
}
pub fn process_msi(
msi_bytes: &[u8],
options: &InstallOptions,
sink: &mut dyn InstallSink,
) -> Result<BTreeMap<String, String>, Error> {
let cursor = Cursor::new(msi_bytes);
let mut pkg = msi::Package::open(cursor).map_err(Error::Open)?;
let mut properties: BTreeMap<String, String> = BTreeMap::new();
if pkg.has_table("Property") {
let rows = pkg
.select_rows(msi::Select::table("Property"))
.map_err(|e| Error::Schema(format!("Property select: {e}")))?;
for row in rows {
let name = row[0].as_str().unwrap_or("").to_string();
let value = row[1].as_str().unwrap_or("").to_string();
if !name.is_empty() {
properties.insert(name, value);
}
}
}
for (k, v) in &options.properties {
properties.insert(k.clone(), v.clone());
}
let names: Vec<String> = properties.keys().cloned().collect();
for name in &names {
if let Some(v) = sink.override_property(name) {
properties.insert(name.clone(), v);
}
}
seed_default_properties(&mut properties);
if !sink.emit(InstallAction::SnapshotProperties(properties.clone())) {
return Ok(properties);
}
let dir_rows = if pkg.has_table("Directory") {
pkg.select_rows(msi::Select::table("Directory"))
.map_err(|e| Error::Schema(format!("Directory select: {e}")))?
} else {
return Ok(properties);
};
let mut dir_parent: BTreeMap<String, String> = BTreeMap::new();
let mut dir_default: BTreeMap<String, String> = BTreeMap::new();
for row in dir_rows {
let id = row[0].as_str().unwrap_or("").to_string();
let parent = row[1].as_str().unwrap_or("").to_string();
let default = row[2].as_str().unwrap_or("").to_string();
if id.is_empty() {
continue;
}
dir_parent.insert(id.clone(), parent);
dir_default.insert(id, default);
}
let mut resolved_dirs: BTreeMap<String, String> = BTreeMap::new();
let dir_ids: Vec<String> = dir_parent.keys().cloned().collect();
for id in &dir_ids {
let path = resolve_dir(
id,
&dir_parent,
&dir_default,
&properties,
&mut resolved_dirs,
options.strict_property_refs,
)?;
if !sink.emit(InstallAction::CreateDirectory {
id: id.clone(),
path,
}) {
return Ok(properties);
}
}
let mut component_dir: BTreeMap<String, String> = BTreeMap::new();
if pkg.has_table("Component") {
let rows = pkg
.select_rows(msi::Select::table("Component"))
.map_err(|e| Error::Schema(format!("Component select: {e}")))?;
for row in rows {
let id = row[0].as_str().unwrap_or("").to_string();
let dir = row[2].as_str().unwrap_or("").to_string();
if !id.is_empty() {
component_dir.insert(id, dir);
}
}
}
let mut media: Vec<(i32, String)> = Vec::new();
if pkg.has_table("Media") {
let rows = pkg
.select_rows(msi::Select::table("Media"))
.map_err(|e| Error::Schema(format!("Media select: {e}")))?;
for row in rows {
let last_sequence = row[1].as_int().unwrap_or(0);
let cabinet = row[3].as_str().unwrap_or("").to_string();
if !cabinet.is_empty() {
media.push((last_sequence, cabinet));
}
}
}
media.sort_by_key(|(seq, _)| *seq);
let mut cab_handles: BTreeMap<String, CabHandle> = BTreeMap::new();
for (_, cab_name) in &media {
if cab_handles.contains_key(cab_name) {
continue;
}
if let Some(stream_name) = cab_name.strip_prefix('#') {
match pkg.read_stream(stream_name) {
Ok(mut reader) => {
let mut buf = Vec::new();
if std::io::Read::read_to_end(&mut reader, &mut buf).is_ok() {
match cab::Cabinet::new(Cursor::new(buf)) {
Ok(cab) => {
cab_handles.insert(cab_name.clone(), CabHandle::Loaded(cab));
}
Err(e) => {
if !sink
.emit(InstallAction::Log(format!("cab open {cab_name}: {e}")))
{
return Ok(properties);
}
cab_handles.insert(cab_name.clone(), CabHandle::Missing);
}
}
}
}
Err(e) => {
if !sink.emit(InstallAction::Log(format!("msi stream {stream_name}: {e}"))) {
return Ok(properties);
}
cab_handles.insert(cab_name.clone(), CabHandle::Missing);
}
}
} else {
if !sink.emit(InstallAction::Log(format!(
"external cab {cab_name} not supported; file bytes will be missing"
))) {
return Ok(properties);
}
cab_handles.insert(cab_name.clone(), CabHandle::Missing);
}
}
if pkg.has_table("File") {
let rows = pkg
.select_rows(msi::Select::table("File"))
.map_err(|e| Error::Schema(format!("File select: {e}")))?;
for row in rows {
let id = row[0].as_str().unwrap_or("").to_string();
let component = row[1].as_str().unwrap_or("").to_string();
let file_name = pick_long_filename(row[2].as_str().unwrap_or(""));
let size = row[3].as_int().unwrap_or(0).max(0) as u64;
let sequence = row[7].as_int().unwrap_or(0);
if component.is_empty() || !sink.install_component(&component) {
continue;
}
let dir_id = component_dir.get(&component).cloned().unwrap_or_default();
let dir_path = resolved_dirs.get(&dir_id).cloned().unwrap_or_default();
let path = join_path(&dir_path, &file_name);
let bytes = extract_file_bytes(&media, &mut cab_handles, sequence, &id);
if !sink.emit(InstallAction::WriteFile {
id,
path,
size,
component,
bytes,
}) {
return Ok(properties);
}
}
}
if pkg.has_table("Registry") {
let rows = pkg
.select_rows(msi::Select::table("Registry"))
.map_err(|e| Error::Schema(format!("Registry select: {e}")))?;
for row in rows {
let id = row[0].as_str().unwrap_or("").to_string();
let root = row[1].as_int().unwrap_or(2);
let key_raw = row[2].as_str().unwrap_or("").to_string();
let name_raw = row[3].as_str().unwrap_or("").to_string();
let value_raw = row[4].as_str().unwrap_or("").to_string();
let component = row[5].as_str().unwrap_or("").to_string();
if component.is_empty() || !sink.install_component(&component) {
continue;
}
let key = expand_properties(&key_raw, &properties, options.strict_property_refs)?;
let name = expand_properties(&name_raw, &properties, options.strict_property_refs)?;
let value_expanded =
expand_properties(&value_raw, &properties, options.strict_property_refs)?;
let value = parse_reg_value(&value_expanded);
if !sink.emit(InstallAction::RegSet {
id,
hive: RegHive::from_msi_root(root),
key,
name,
value,
component,
}) {
return Ok(properties);
}
}
}
Ok(properties)
}
fn seed_default_properties(props: &mut BTreeMap<String, String>) {
let defaults: &[(&str, &str)] = &[
("TARGETDIR", "C:\\"),
("SourceDir", "C:\\"),
("ProgramFilesFolder", "C:\\Program Files"),
("ProgramFiles64Folder", "C:\\Program Files"),
("CommonFilesFolder", "C:\\Program Files\\Common Files"),
("CommonFiles64Folder", "C:\\Program Files\\Common Files"),
("WindowsFolder", "C:\\Windows"),
("System64Folder", "C:\\Windows\\System32"),
("SystemFolder", "C:\\Windows\\System32"),
("System16Folder", "C:\\Windows\\System"),
("WindowsVolume", "C:\\"),
("AppDataFolder", "C:\\Users\\Default\\AppData\\Roaming"),
("LocalAppDataFolder", "C:\\Users\\Default\\AppData\\Local"),
("DesktopFolder", "C:\\Users\\Public\\Desktop"),
(
"StartMenuFolder",
"C:\\ProgramData\\Microsoft\\Windows\\Start Menu",
),
(
"ProgramMenuFolder",
"C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs",
),
("TempFolder", "C:\\Temp"),
("PersonalFolder", "C:\\Users\\Public\\Documents"),
("UserProfile", "C:\\Users\\Default"),
("ComputerName", "OXIDEAV"),
("USERNAME", "Default"),
("VersionNT", "603"),
("VersionNT64", "603"),
("Privileged", "1"),
];
for (name, value) in defaults {
props
.entry((*name).into())
.or_insert_with(|| (*value).into());
}
}
fn resolve_dir(
id: &str,
parents: &BTreeMap<String, String>,
defaults: &BTreeMap<String, String>,
properties: &BTreeMap<String, String>,
resolved: &mut BTreeMap<String, String>,
strict: bool,
) -> Result<String, Error> {
if let Some(p) = resolved.get(id) {
return Ok(p.clone());
}
let parent = parents.get(id).cloned().unwrap_or_default();
let default = defaults.get(id).cloned().unwrap_or_default();
let mut segment = parse_default_dir(&default);
segment = expand_properties(&segment, properties, strict)?;
let absolute = if parent.is_empty() || parent == id {
if let Some(v) = properties.get(id) {
v.clone()
} else {
segment
}
} else {
let parent_path = resolve_dir(&parent, parents, defaults, properties, resolved, strict)?;
join_path(&parent_path, &segment)
};
resolved.insert(id.to_string(), absolute.clone());
Ok(absolute)
}
fn parse_default_dir(s: &str) -> String {
let before_colon = s.split(':').next().unwrap_or("");
let long = before_colon.split('|').next_back().unwrap_or(before_colon);
if long == "." {
String::new()
} else {
long.to_string()
}
}
fn pick_long_filename(s: &str) -> String {
let long = s.split('|').next_back().unwrap_or(s);
long.to_string()
}
fn join_path(parent: &str, child: &str) -> String {
if child.is_empty() {
return parent.to_string();
}
if parent.is_empty() {
return child.to_string();
}
let parent_trim = parent.trim_end_matches('\\').trim_end_matches('/');
format!("{parent_trim}\\{child}")
}
fn expand_properties(
s: &str,
props: &BTreeMap<String, String>,
strict: bool,
) -> Result<String, Error> {
if !s.contains('[') {
return Ok(s.to_string());
}
let mut out = String::with_capacity(s.len());
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'[' {
if let Some(close) = s[i + 1..].find(']') {
let inside = &s[i + 1..i + 1 + close];
let first = inside.chars().next().unwrap_or(' ');
if matches!(first, '~' | '\\' | '!' | '#' | '$' | '%') {
out.push('[');
out.push_str(inside);
out.push(']');
} else if let Some(v) = props.get(inside) {
out.push_str(v);
} else if strict {
return Err(Error::UnresolvedProperty(inside.to_string()));
} else {
out.push('[');
out.push_str(inside);
out.push(']');
}
i = i + 1 + close + 1;
continue;
}
}
out.push(bytes[i] as char);
i += 1;
}
let mut chained = out;
for _ in 0..8 {
if !chained.contains('[') {
break;
}
let next = expand_once(&chained, props, strict)?;
if next == chained {
break;
}
chained = next;
}
Ok(chained)
}
fn expand_once(s: &str, props: &BTreeMap<String, String>, strict: bool) -> Result<String, Error> {
let mut out = String::with_capacity(s.len());
let bytes = s.as_bytes();
let mut i = 0;
let mut changed = false;
while i < bytes.len() {
if bytes[i] == b'[' {
if let Some(close) = s[i + 1..].find(']') {
let inside = &s[i + 1..i + 1 + close];
let first = inside.chars().next().unwrap_or(' ');
if !matches!(first, '~' | '\\' | '!' | '#' | '$' | '%') {
if let Some(v) = props.get(inside) {
out.push_str(v);
i = i + 1 + close + 1;
changed = true;
continue;
} else if strict {
return Err(Error::UnresolvedProperty(inside.to_string()));
}
}
out.push('[');
out.push_str(inside);
out.push(']');
i = i + 1 + close + 1;
continue;
}
}
out.push(bytes[i] as char);
i += 1;
}
if changed {
Ok(out)
} else {
Ok(s.to_string())
}
}
fn parse_reg_value(s: &str) -> RegValue {
if s.is_empty() {
return RegValue::Empty;
}
if let Some(rest) = s.strip_prefix("#%") {
return RegValue::ExpandSz(rest.to_string());
}
if let Some(rest) = s.strip_prefix("#x") {
let mut bytes = Vec::with_capacity(rest.len() / 2);
let chars: Vec<char> = rest.chars().collect();
let mut i = 0;
while i + 1 < chars.len() {
let hi = chars[i].to_digit(16).unwrap_or(0) as u8;
let lo = chars[i + 1].to_digit(16).unwrap_or(0) as u8;
bytes.push(hi << 4 | lo);
i += 2;
}
return RegValue::Binary(bytes);
}
if let Some(rest) = s.strip_prefix('#') {
let n: i64 = rest.parse().unwrap_or(0);
return RegValue::Dword(n as u32);
}
if s.contains("[~]") {
let parts: Vec<String> = s
.split("[~]")
.filter(|p| !p.is_empty())
.map(|p| p.to_string())
.collect();
return RegValue::MultiSz(parts);
}
RegValue::Sz(s.to_string())
}
enum CabHandle {
Loaded(cab::Cabinet<Cursor<Vec<u8>>>),
Missing,
}
fn extract_file_bytes(
media: &[(i32, String)],
cabs: &mut BTreeMap<String, CabHandle>,
sequence: i32,
file_id: &str,
) -> Option<Vec<u8>> {
let cab_name = media
.iter()
.find(|(last, _)| *last >= sequence)
.map(|(_, name)| name.clone())
.or_else(|| media.first().map(|(_, n)| n.clone()))?;
let handle = cabs.get_mut(&cab_name)?;
let cab = match handle {
CabHandle::Loaded(c) => c,
CabHandle::Missing => return None,
};
let mut reader = cab.read_file(file_id).ok()?;
let mut buf = Vec::new();
if std::io::Read::read_to_end(&mut reader, &mut buf).is_err() {
return None;
}
Some(buf)
}
#[derive(Debug, Default)]
pub struct RecordingSink {
pub actions: Vec<InstallAction>,
}
impl InstallSink for RecordingSink {
fn emit(&mut self, action: InstallAction) -> bool {
self.actions.push(action);
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_default_dir_picks_long_target() {
assert_eq!(parse_default_dir("DOC|Documents"), "Documents");
assert_eq!(parse_default_dir("DOC|Documents:SRC|Source"), "Documents");
assert_eq!(parse_default_dir("SourceDir"), "SourceDir");
assert_eq!(parse_default_dir("."), "");
}
#[test]
fn expand_properties_substitutes_known() {
let mut props = BTreeMap::new();
props.insert("ProgramFilesFolder".into(), "C:\\Program Files".into());
props.insert("Manufacturer".into(), "Apple".into());
let s = "[ProgramFilesFolder]\\[Manufacturer]\\Foo";
assert_eq!(
expand_properties(s, &props, false).unwrap(),
"C:\\Program Files\\Apple\\Foo"
);
}
#[test]
fn expand_properties_leaves_unknown_in_lenient_mode() {
let props = BTreeMap::new();
let s = "[Bogus]\\file";
assert_eq!(
expand_properties(s, &props, false).unwrap(),
"[Bogus]\\file"
);
assert!(matches!(
expand_properties(s, &props, true).unwrap_err(),
Error::UnresolvedProperty(_)
));
}
#[test]
fn parse_reg_value_decodes_each_sigil() {
assert!(matches!(parse_reg_value(""), RegValue::Empty));
assert!(matches!(parse_reg_value("hello"), RegValue::Sz(ref s) if s == "hello"));
assert!(matches!(parse_reg_value("#42"), RegValue::Dword(42)));
assert!(matches!(parse_reg_value("#%path"), RegValue::ExpandSz(ref s) if s == "path"));
let RegValue::Binary(b) = parse_reg_value("#xDEADBEEF") else {
panic!("expected binary");
};
assert_eq!(b, vec![0xDE, 0xAD, 0xBE, 0xEF]);
let RegValue::MultiSz(v) = parse_reg_value("[~]a[~]b[~]") else {
panic!("expected multi-sz");
};
assert_eq!(v, vec!["a".to_string(), "b".to_string()]);
}
#[test]
fn reg_hive_from_msi_root_maps_canonical() {
assert_eq!(RegHive::from_msi_root(0), RegHive::ClassesRoot);
assert_eq!(RegHive::from_msi_root(1), RegHive::CurrentUser);
assert_eq!(RegHive::from_msi_root(2), RegHive::LocalMachine);
assert_eq!(RegHive::from_msi_root(3), RegHive::Users);
assert_eq!(RegHive::from_msi_root(-1), RegHive::LocalMachine);
}
#[test]
fn parse_command_line_extracts_install_path() {
let (op, props) =
parse_msiexec_args("\"C:\\WINDOWS\\System32\\msiexec.exe\" /i \"C:\\foo\\bar.msi\" /quiet PROP=VAL OTHER=1");
assert_eq!(op, Some(MsiexecOp::Install("C:\\foo\\bar.msi".into())));
assert_eq!(props.get("PROP").map(String::as_str), Some("VAL"));
assert_eq!(props.get("OTHER").map(String::as_str), Some("1"));
}
#[test]
fn is_msiexec_target_recognises_canonical_paths() {
assert!(is_msiexec_target("C:\\Windows\\System32\\msiexec.exe"));
assert!(is_msiexec_target("C:\\WINDOWS\\System32\\MSIEXEC.EXE"));
assert!(is_msiexec_target("msiexec.exe"));
assert!(is_msiexec_target("/usr/share/wine/msiexec"));
assert!(!is_msiexec_target("C:\\foo\\setup.exe"));
}
}
#[must_use]
pub fn is_msiexec_target(target: &str) -> bool {
let lower = target.to_ascii_lowercase().replace('\\', "/");
let tail = lower.rsplit('/').next().unwrap_or(&lower);
tail == "msiexec.exe" || tail == "msiexec"
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MsiexecOp {
Install(String),
Uninstall(String),
}
pub fn parse_msiexec_args(cmd: &str) -> (Option<MsiexecOp>, BTreeMap<String, String>) {
let mut tokens: Vec<String> = Vec::new();
let mut cur = String::new();
let mut in_quote = false;
for ch in cmd.chars() {
match ch {
'"' => in_quote = !in_quote,
c if c.is_whitespace() && !in_quote => {
if !cur.is_empty() {
tokens.push(std::mem::take(&mut cur));
}
}
c => cur.push(c),
}
}
if !cur.is_empty() {
tokens.push(cur);
}
let mut iter = tokens.into_iter().peekable();
if let Some(first) = iter.peek() {
if first.to_ascii_lowercase().contains("msiexec") {
iter.next();
}
}
let mut op = None;
let mut props = BTreeMap::new();
while let Some(tok) = iter.next() {
let lower = tok.to_ascii_lowercase();
match lower.as_str() {
"/i" | "-i" => {
if let Some(path) = iter.next() {
op = Some(MsiexecOp::Install(path));
}
}
"/x" | "-x" => {
if let Some(path) = iter.next() {
op = Some(MsiexecOp::Uninstall(path));
}
}
_ if tok.contains('=') => {
if let Some((k, v)) = tok.split_once('=') {
props.insert(k.to_string(), v.to_string());
}
}
_ => {}
}
}
(op, props)
}
struct EmulatorInstallSink<'a> {
vfs: Option<&'a mut crate::context::VirtualFs>,
registry: Option<&'a mut crate::context::VirtualRegistry>,
log: Vec<String>,
n_dirs: usize,
n_files: usize,
n_regs: usize,
n_bytes: u64,
n_real_bytes: u64,
}
impl InstallSink for EmulatorInstallSink<'_> {
fn emit(&mut self, action: InstallAction) -> bool {
match action {
InstallAction::CreateDirectory { path, .. } => {
self.n_dirs += 1;
if let Some(vfs) = self.vfs.as_mut() {
let marker = format!("{}\\.dir", path.trim_end_matches(['\\', '/']));
if !vfs.contains(&marker) {
vfs.insert(&marker, Vec::new());
}
}
}
InstallAction::WriteFile {
path, size, bytes, ..
} => {
self.n_files += 1;
self.n_bytes = self.n_bytes.saturating_add(size);
if let Some(vfs) = self.vfs.as_mut() {
let payload = bytes.unwrap_or_default();
if !payload.is_empty() {
self.n_real_bytes = self.n_real_bytes.saturating_add(payload.len() as u64);
}
vfs.write_path(&path, payload);
}
}
InstallAction::RegSet {
hive,
key,
name,
value,
..
} => {
self.n_regs += 1;
if let Some(reg) = self.registry.as_mut() {
let key_path = format!("{}\\{}", hive.short(), key);
let v = match value {
RegValue::Empty => crate::context::RegistryValue::Sz(String::new()),
RegValue::Sz(s) => crate::context::RegistryValue::Sz(s),
RegValue::ExpandSz(s) => crate::context::RegistryValue::ExpandSz(s),
RegValue::Dword(d) => crate::context::RegistryValue::Dword(d),
RegValue::Binary(b) => crate::context::RegistryValue::Binary(b),
RegValue::MultiSz(v) => crate::context::RegistryValue::MultiSz(v),
};
reg.set_value(&key_path, &name, v);
}
}
InstallAction::SnapshotProperties(props) => {
self.log.push(format!(
"msiexec: property snapshot ({} entries)",
props.len()
));
}
InstallAction::Log(line) => {
self.log.push(format!("msiexec: {line}"));
}
}
true
}
}
pub fn dispatch_msiexec_install(
state: &mut crate::win32::HostState,
_mmu: &mut crate::emulator::Mmu,
target: &str,
cmdline: &str,
) {
let (op, mut props) = parse_msiexec_args(cmdline);
let Some(verb) = op else {
state.debug_log.push(format!(
"msiexec({target:?}): no install/uninstall verb in cmdline {cmdline:?}"
));
return;
};
let msi_path = match &verb {
MsiexecOp::Install(p) => p.clone(),
MsiexecOp::Uninstall(p) => {
state
.debug_log
.push(format!("msiexec /x {p:?} — uninstall is a no-op for now"));
return;
}
};
let bytes = lookup_msi_bytes(&state.context, &msi_path);
let Some(bytes) = bytes else {
state.debug_log.push(format!(
"msiexec /i {msi_path:?} — MSI not in VFS, install skipped"
));
return;
};
let options = InstallOptions {
properties: std::mem::take(&mut props),
..Default::default()
};
let mut vfs = state.context.vfs.take();
let mut reg = state.context.registry.take();
let mut sink = EmulatorInstallSink {
vfs: vfs.as_mut(),
registry: reg.as_mut(),
log: Vec::new(),
n_dirs: 0,
n_files: 0,
n_regs: 0,
n_bytes: 0,
n_real_bytes: 0,
};
let result = process_msi(&bytes, &options, &mut sink);
let dirs = sink.n_dirs;
let files = sink.n_files;
let regs = sink.n_regs;
let bytes_total = sink.n_bytes;
let real_bytes = sink.n_real_bytes;
for line in sink.log {
state.debug_log.push(line);
}
state.context.vfs = vfs;
state.context.registry = reg;
match result {
Ok(_) => state.debug_log.push(format!(
"msiexec /i {msi_path:?} — synthesised {files} files ({real_bytes}/{bytes_total} bytes extracted), {dirs} directories, {regs} registry entries"
)),
Err(e) => state
.debug_log
.push(format!("msiexec /i {msi_path:?} — walk failed: {e}")),
}
}
fn lookup_msi_bytes(ctx: &crate::context::Context, path: &str) -> Option<Vec<u8>> {
let vfs = ctx.vfs.as_ref()?;
if let Some(b) = vfs.read(path) {
return Some(b.to_vec());
}
let stripped = path
.trim_start_matches("C:\\")
.trim_start_matches("c:\\")
.trim_start_matches("C:/")
.trim_start_matches("c:/");
if stripped != path {
if let Some(b) = vfs.read(stripped) {
return Some(b.to_vec());
}
}
let needle = path
.rsplit(['\\', '/'])
.next()
.unwrap_or(path)
.to_ascii_lowercase();
for (vpath, _) in vfs.list() {
if vpath.to_ascii_lowercase().ends_with(&needle) {
return vfs.read(vpath).map(<[u8]>::to_vec);
}
}
None
}