Skip to content

Commit 7f8f858

Browse files
committed
squash: feat/analyse-qt
1 parent 87cd45c commit 7f8f858

3 files changed

Lines changed: 163 additions & 2 deletions

File tree

src/analysis/installers/exe.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ use winget_types::installer::{Architecture, Installer, InstallerSwitches, Instal
77
use yara_x::mods::PE;
88

99
use super::{
10-
super::Installers, AdvancedInstaller, Burn, InstallShield, Nsis, SevenZipSfx, Squirrel,
10+
super::Installers, AdvancedInstaller, Burn, InstallShield, Nsis, Qt, SevenZipSfx, Squirrel,
1111
installshield::InstallShieldError,
1212
};
1313
use crate::{
1414
analysis::installers::{
15-
advanced::AdvancedInstallerError, burn::BurnError, nsis::NsisError,
15+
advanced::AdvancedInstallerError, burn::BurnError, nsis::NsisError, qt::QtError,
1616
sevenzip_sfx::SevenZipSfxError, squirrel::SquirrelError,
1717
},
1818
traits::FromMachine,
@@ -28,6 +28,7 @@ pub enum Exe {
2828
Inno(Box<Inno>),
2929
InstallShield(Box<InstallShield>),
3030
Nsis(Nsis),
31+
Qt(Qt),
3132
SevenZipSfx(Box<SevenZipSfx>),
3233
Squirrel(Squirrel),
3334
Generic(Box<Installer>),
@@ -65,6 +66,12 @@ impl Exe {
6566
Err(error) => return Err(error.into()),
6667
}
6768

69+
match Qt::new(&mut reader, pe) {
70+
Ok(qt) => return Ok(Self::Qt(qt)),
71+
Err(QtError::NotQtFile) => {}
72+
Err(error) => return Err(error.into()),
73+
}
74+
6875
match SevenZipSfx::new(&mut reader, pe) {
6976
Ok(sfx) => return Ok(Self::SevenZipSfx(Box::new(sfx))),
7077
Err(SevenZipSfxError::NotSevenZipSfx) => {}
@@ -135,6 +142,7 @@ impl Installers for Exe {
135142
Self::Inno(inno) => inno.installers(),
136143
Self::InstallShield(installshield) => installshield.installers(),
137144
Self::Nsis(nsis) => nsis.installers(),
145+
Self::Qt(qt) => qt.installers(),
138146
Self::SevenZipSfx(sfx) => sfx.installers(),
139147
Self::Squirrel(squirrel) => squirrel.installers(),
140148
Self::Generic(installer) => vec![*installer.clone()],

src/analysis/installers/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ mod installshield;
66
mod msi;
77
pub mod msix_family;
88
pub mod nsis;
9+
mod qt;
910
mod sevenzip_sfx;
1011
pub mod squirrel;
1112
pub mod utils;
@@ -17,6 +18,7 @@ pub use exe::Exe;
1718
pub use installshield::InstallShield;
1819
pub use msi::Msi;
1920
pub use nsis::Nsis;
21+
pub use qt::Qt;
2022
pub use sevenzip_sfx::SevenZipSfx;
2123
pub use squirrel::Squirrel;
2224
pub use zip::Zip;

src/analysis/installers/qt.rs

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
use std::{
2+
collections::BTreeSet,
3+
io::{Read, Seek, SeekFrom},
4+
};
5+
6+
use byteorder::{BigEndian, ReadBytesExt};
7+
use serde::Deserialize;
8+
use thiserror::Error;
9+
use winget_types::{
10+
Version,
11+
installer::{
12+
AppsAndFeaturesEntries, AppsAndFeaturesEntry, Architecture, ExpectedReturnCodes,
13+
InstallModes, Installer, InstallerReturnCode, InstallerSwitches, InstallerType,
14+
ReturnResponse,
15+
},
16+
};
17+
use yara_x::mods::PE;
18+
19+
use crate::{analysis::Installers, traits::FromMachine};
20+
21+
#[derive(Error, Debug)]
22+
pub enum QtError {
23+
#[error("Not a Qt Installer Framework installer")]
24+
NotQtFile,
25+
#[error(transparent)]
26+
Io(#[from] std::io::Error),
27+
}
28+
29+
#[derive(Debug, Deserialize)]
30+
#[serde(rename = "Updates")]
31+
struct Updates {
32+
#[serde(rename = "ApplicationName")]
33+
application_name: Option<String>,
34+
#[serde(rename = "ApplicationVersion")]
35+
application_version: Option<String>,
36+
#[serde(rename = "PackageUpdate")]
37+
package_updates: Option<Vec<PackageUpdate>>,
38+
}
39+
40+
#[derive(Debug, Deserialize)]
41+
struct PackageUpdate {
42+
#[serde(rename = "DisplayName")]
43+
display_name: Option<String>,
44+
#[serde(rename = "Version")]
45+
version: Option<String>,
46+
}
47+
48+
pub struct Qt {
49+
architecture: Architecture,
50+
updates: Updates,
51+
}
52+
53+
impl Qt {
54+
// Detects Qt Installer Framework (IFW) by parsing Updates.xml from the PE overlay's QT resource (qres)
55+
// Installer config.xml has Publisher and DefaultInstallDirectory, but we'd need to traverse the file tree
56+
pub fn new<R: Read + Seek>(mut reader: R, pe: &PE) -> Result<Self, QtError> {
57+
let overlay_offset = pe.overlay.offset.ok_or(QtError::NotQtFile)?;
58+
59+
reader.seek(SeekFrom::Start(overlay_offset))?;
60+
61+
let mut magic = [0u8; 4];
62+
reader.read_exact(&mut magic)?;
63+
if &magic != b"qres" {
64+
return Err(QtError::NotQtFile);
65+
}
66+
67+
reader.seek(SeekFrom::Current(8))?; // Skip version and tree_offset
68+
let data_offset = reader.read_u32::<BigEndian>()?;
69+
70+
reader.seek(SeekFrom::Start(overlay_offset + u64::from(data_offset)))?;
71+
72+
let size = reader.read_u32::<BigEndian>()? as usize;
73+
let mut data = vec![0u8; size];
74+
reader.read_exact(&mut data)?;
75+
76+
let updates: Updates =
77+
quick_xml::de::from_str(std::str::from_utf8(&data).map_err(|_| QtError::NotQtFile)?)
78+
.map_err(|_| QtError::NotQtFile)?;
79+
80+
Ok(Self {
81+
architecture: Architecture::from_machine(pe.machine()),
82+
updates,
83+
})
84+
}
85+
}
86+
87+
impl Installers for Qt {
88+
fn installers(&self) -> Vec<Installer> {
89+
let package = self
90+
.updates
91+
.package_updates
92+
.as_ref()
93+
.and_then(|p| p.first());
94+
95+
let display_name = self
96+
.updates
97+
.application_name
98+
.clone()
99+
.or_else(|| package.and_then(|p| p.display_name.clone()));
100+
101+
let version = self
102+
.updates
103+
.application_version
104+
.clone()
105+
.or_else(|| package.and_then(|p| p.version.clone()));
106+
107+
vec![Installer {
108+
architecture: self.architecture,
109+
r#type: Some(InstallerType::Exe),
110+
install_modes: InstallModes::all(),
111+
switches: InstallerSwitches::builder()
112+
.silent(
113+
"install --accept-licenses --accept-messages --confirm-command --default-answer"
114+
.parse()
115+
.unwrap(),
116+
)
117+
.silent_with_progress(
118+
"install --accept-licenses --accept-messages --confirm-command --default-answer"
119+
.parse()
120+
.unwrap(),
121+
)
122+
.install_location("--root \"<INSTALLPATH>\"".parse().unwrap())
123+
.build(),
124+
expected_return_codes: expected_return_codes(),
125+
apps_and_features_entries: AppsAndFeaturesEntries::from(
126+
AppsAndFeaturesEntry::builder()
127+
.maybe_display_name(display_name)
128+
.maybe_display_version(version.and_then(|v| v.parse::<Version>().ok()))
129+
.build(),
130+
),
131+
..Installer::default()
132+
}]
133+
}
134+
}
135+
136+
// https://doc.qt.io/qtinstallerframework/qinstaller-packagemanagercore.html#Status-enum
137+
fn expected_return_codes() -> BTreeSet<ExpectedReturnCodes> {
138+
use ReturnResponse::*;
139+
[
140+
(1, ContactSupport),
141+
(2, InstallInProgress),
142+
(3, CancelledByUser),
143+
]
144+
.into_iter()
145+
.map(|(code, response)| ExpectedReturnCodes {
146+
installer_return_code: InstallerReturnCode::new(code),
147+
return_response: response,
148+
return_response_url: None,
149+
})
150+
.collect()
151+
}

0 commit comments

Comments
 (0)