Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/rust
{
"name": "Rust",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/rust:1-1-bookworm"

// Use 'mounts' to make the cargo cache persistent in a Docker Volume.
// "mounts": [
// {
// "source": "devcontainer-cargo-cache-${devcontainerId}",
// "target": "/usr/local/cargo",
// "type": "volume"
// }
// ]

// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},

// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],

// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "rustc --version",

// Configure tool-specific properties.
// "customizations": {},

// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "vendor/inno"]
path = vendor/inno
url = https://github.com/russellbanks/inno
48 changes: 48 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug executable 'komac'",
"cargo": {
"args": [
"build",
"--bin=komac",
"--package=komac"
],
"filter": {
"name": "komac",
"kind": "bin"
}
},
"args": [
"analyse",
".idea/installshield/AllShare_2.1.0.12031_10.exe"
],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in executable 'komac'",
"cargo": {
"args": [
"test",
"--no-run",
"--bin=komac",
"--package=komac"
],
"filter": {
"name": "komac",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
}
]
}
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"files.autoSave": "off"
}
33 changes: 30 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ Contributions are what make the open source community such an amazing place to l

### Testing your changes

Using Docker is the easiest way to to test your code before submitting a pull request.
Using Docker is the easiest way to to test your code before submitting a pull request.

> [!NOTE]
> When using the Docker container on Windows, the WSL engine does not support the default collection for keys or tokens. This means that when testing inside the container GitHub tokens will not be stored, even when `komac token update` is used.
>
>
> This is a [known issue](https://github.com/hwchen/keyring-rs/blob/47c8daf3e6178a2282ae3e8670d1ea7fa736b8cb/src/secret_service.rs#L73-L77) which is documented in the keyring crate.
>
> As a workaround, you can set the `GITHUB_TOKEN` environment variable from within the container, in the `docker run` command, or in the Dockerfile itself
Expand All @@ -30,4 +30,31 @@ Using Docker is the easiest way to to test your code before submitting a pull re
2. Run `docker build ./ --tag komac_dev:latest`.
3. Wait for the build to complete.
4. Start the container using `docker run -it komac_dev bash`.
5. Test out any commands. Use the `exit` command to quit the container
5. Test out any commands. Use the `exit` command to quit the container

### Known Bugs

#### Burn

| Error | Examples | Comment |
|---------|---------------------|---------|
| `Expected RParen, got Some(Ident("or"))` | python-3.12.9-amd64.exe | Can't parse InstallCondition with implied evaluation eg A or B or C |

#### Inno

The error message is typically `failed to fill whole buffer` with no further details.

| Version | Examples | Comment |
|---------|---------------------|---------|
| 5570 | BHPIO CAD Build v9.1 for Microstation Connect 060522.exe | |
| 5500u | ClassicStickyNotes-2.0-setup.exe | |
| 3061 | e-bility5.00.64g.exe | |
| 4260 | MoffFreeCalcSetup.exe | |
| 5570 | Ultranalysis Suite 3 Base Setup.exe | |

#### NSIS

| Error | Examples | Comment |
|---------|---------------------|---------|
| `The conversion failed because the source bytes are not a valid value of the destination type. Destination type: [komac::installers::nsis::entry::Entry]` | iTwinStudioSetup.exe | |
| Leading unicode in `DefaultInstallLocation` | mbbServiceSetup.exe<br>scopephoto.exe | Failing to parse app root? |
15 changes: 13 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,9 @@ supports-hyperlinks = "3.1.0"
tempfile = "3.23.0"
thiserror = "2.0.17"
tokio = { version = "1.48.0", features = ["rt-multi-thread", "macros", "fs", "parking_lot"] }
tracing = { version = "0.1.41", features = ["release_max_level_warn"] }
tracing = "0.1.41"
tracing-indicatif = "0.3.13"
tracing-subscriber = "0.3.20"
tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }
tree-sitter-highlight = "0.25.10"
tree-sitter-yaml = "0.7.2"
tui-textarea = { version = "0.7.0", features = ["search"] }
Expand Down Expand Up @@ -110,3 +110,6 @@ rstest = "0.26.1"
assets = [
{ source = "target/release/komac", dest = "/usr/bin/komac", mode = "755" },
]

[patch.crates-io]
inno = { path = "vendor/inno/core" }
1 change: 1 addition & 0 deletions src/analysis/extensions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ pub const APPX: &str = "appx";
pub const MSIX_BUNDLE: &str = "msixbundle";
pub const APPX_BUNDLE: &str = "appxbundle";
pub const ZIP: &str = "zip";
pub const APPINSTALLER: &str = "appinstaller";
25 changes: 24 additions & 1 deletion src/analysis/installers/burn/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use thiserror::Error;
use tracing::debug;
use winget_types::installer::{
AppsAndFeaturesEntries, AppsAndFeaturesEntry, Architecture, InstallationMetadata, Installer,
InstallerType, Scope,
InstallerSwitches, InstallerType, Scope,
};
use wix_burn_stub::WixBurnStub;
use yara_x::mods::{
Expand Down Expand Up @@ -213,6 +213,29 @@ impl Installers for Burn {
..InstallationMetadata::default()
})
.unwrap_or_default(),
switches: InstallerSwitches::builder()
.maybe_custom({
let mut switches = manifest
.variables
.iter()
.map(|variable| {
format!(
"{}=\"{}\"",
variable.id(),
variable
.resolved_value()
.unwrap_or_default()
.replace("NOT_SET", "")
)
})
// Exclude some built-in variables
// https://docs.firegiant.com/wix3/bundle/bundle_built_in_variables/
.filter(|switch| !switch.starts_with("WixBundle"))
.collect::<Vec<_>>();
switches.sort();
switches.join(" ").parse().ok()
})
.build(),
..Installer::default()
}]
}
Expand Down
64 changes: 53 additions & 11 deletions src/analysis/installers/exe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,24 @@ use std::io::{Read, Seek};

use color_eyre::Result;
use inno::{Inno, error::InnoError};
use winget_types::installer::{Architecture, Installer, InstallerType};
use winget_types::installer::{Architecture, Installer, InstallerSwitches, InstallerType};
use yara_x::mods::PE;

use super::{super::Installers, Burn, Nsis};
use super::{super::Installers, Burn, InstallShield, Nsis};
use crate::{
analysis::installers::{burn::BurnError, nsis::NsisError},
analysis::installers::{burn::BurnError, installshield::InstallShieldError, nsis::NsisError},
traits::FromMachine,
};

const ORIGINAL_FILENAME: &str = "OriginalFilename";
const INTERNAL_NAME: &str = "InternalName";
const FILE_DESCRIPTION: &str = "FileDescription";
const BASIC_INSTALLER_KEYWORDS: [&str; 4] = ["installer", "setup", "7zs.sfx", "7zsd.sfx"];
const BASIC_INSTALLER_KEYWORDS: [&str; 2] = ["installer", "setup"];

pub enum Exe {
Burn(Box<Burn>),
Inno(Box<Inno>),
InstallShield(Box<InstallShield>),
Nsis(Nsis),
Generic(Box<Installer>),
}
Expand All @@ -36,15 +38,46 @@ impl Exe {
Err(error) => return Err(error.into()),
}

match InstallShield::new(&mut reader, pe) {
Ok(installshield) => return Ok(Self::InstallShield(Box::new(installshield))),
Err(InstallShieldError::NotInstallShieldFile) => {}
Err(error) => return Err(error.into()),
}

match Nsis::new(&mut reader, pe) {
Ok(nsis) => return Ok(Self::Nsis(nsis)),
Err(NsisError::NotNsisFile) => {}
Err(error) => return Err(error.into()),
}

Ok(Self::Generic(Box::new(Installer {
architecture: Architecture::from_machine(pe.machine()),
r#type: if pe
let internal_name = pe
.version_info_list
.iter()
.find(|key_value| key_value.key() == INTERNAL_NAME)
.and_then(|key_value| key_value.value.as_deref())
.map(str::to_ascii_lowercase)
.unwrap_or_default();

let switches = match internal_name.as_str() {
"sfxcab.exe" => InstallerSwitches::builder()
.silent("/quiet".parse().unwrap())
.build(),
"7zs.sfx" | "7z.sfx" | "7zsd.sfx" => InstallerSwitches::builder()
.silent("/s".parse().unwrap())
.build(),
"setup launcher" => InstallerSwitches::builder()
.silent("/s".parse().unwrap())
.build(),
"wextract" => InstallerSwitches::builder()
.silent("/Q".parse().unwrap())
.build(),
_ => InstallerSwitches::default(),
};

let installer_type = if switches.silent().is_some() {
InstallerType::Exe
} else {
let is_installer = pe
.version_info_list
.iter()
.filter(|key_value| matches!(key_value.key(), FILE_DESCRIPTION | ORIGINAL_FILENAME))
Expand All @@ -53,11 +86,19 @@ impl Exe {
BASIC_INSTALLER_KEYWORDS
.iter()
.any(|keyword| value.contains(keyword))
}) {
Some(InstallerType::Exe)
});

if is_installer {
InstallerType::Exe
} else {
Some(InstallerType::Portable)
},
InstallerType::Portable
}
};

Ok(Self::Generic(Box::new(Installer {
architecture: Architecture::from_machine(pe.machine()),
r#type: Some(installer_type),
switches,
..Installer::default()
})))
}
Expand All @@ -68,6 +109,7 @@ impl Installers for Exe {
match self {
Self::Burn(burn) => burn.installers(),
Self::Inno(inno) => inno.installers(),
Self::InstallShield(installshield) => installshield.installers(),
Self::Nsis(nsis) => nsis.installers(),
Self::Generic(installer) => vec![*installer.clone()],
}
Expand Down
19 changes: 19 additions & 0 deletions src/analysis/installers/inno/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use inno::{
header::{Architecture as InnoArchitecture, PrivilegesRequiredOverrides},
};
use msi::Language as CodePageLanguage;
use regex::Regex;
use winget_types::{
LanguageTag, Sha256String,
installer::{
Expand Down Expand Up @@ -72,6 +73,24 @@ impl Installers for Inno {
default_install_location: install_dir.map(Utf8PathBuf::from),
..InstallationMetadata::default()
},
switches: InstallerSwitches::builder()
.maybe_custom({
let param_regex = Regex::new(r"\{param:([^|]+)\(|([^}]+)\)?}").unwrap();
let header_str = format!("{:?}", self.header);
let mut switches = param_regex
.captures_iter(&header_str)
.filter_map(|caps| {
Some(format!(
"/{}=\"{}\"",
caps.get(1)?.as_str(),
caps.get(2)?.as_str()
))
})
.collect::<Vec<_>>();
switches.sort();
switches.join(" ").parse().ok()
})
.build(),
..Default::default()
};

Expand Down
Loading
Loading