From abb558b106395ce8dea4e3c592d5c008dc14b18f Mon Sep 17 00:00:00 2001 From: Ryze <50497128+ryze312@users.noreply.github.com> Date: Mon, 28 Aug 2023 16:06:57 +0300 Subject: [PATCH] Initial release --- .cargo/config.toml | 2 + .gitignore | 1 + Cargo.lock | 466 ++++++++++++++++++++++++++++++++++ Cargo.toml | 16 ++ README.md | 50 ++++ src/error.rs | 30 +++ src/install.rs | 3 + src/install/firefox.rs | 41 +++ src/lib.rs | 9 + src/main.rs | 45 ++++ src/models.rs | 146 +++++++++++ src/uninstall.rs | 27 ++ src/uninstall/chrome_based.rs | 25 ++ src/uninstall/edge_based.rs | 78 ++++++ src/uninstall/opera_based.rs | 48 ++++ src/utils.rs | 103 ++++++++ 16 files changed, 1090 insertions(+) create mode 100644 .cargo/config.toml create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 src/error.rs create mode 100644 src/install.rs create mode 100644 src/install/firefox.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/models.rs create mode 100644 src/uninstall.rs create mode 100644 src/uninstall/chrome_based.rs create mode 100644 src/uninstall/edge_based.rs create mode 100644 src/uninstall/opera_based.rs create mode 100644 src/utils.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..ba54493 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "x86_64-pc-windows-gnu" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..0fdcaf4 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,466 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + +[[package]] +name = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + +[[package]] +name = "cc" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "dechrome" +version = "0.9.0" +dependencies = [ + "is_elevated", + "ureq", + "winreg", +] + +[[package]] +name = "flate2" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "is_elevated" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5299060ff5db63e788015dcb9525ad9b84f4fd9717ed2cbdeba5018cbf42f9b5" +dependencies = [ + "winapi", +] + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "log" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "rustls" +version = "0.21.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1feddffcfcc0b33f5c6ce9a29e341e4cd59c3f78e7ee45f4a40c038b1d6cbb" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.3", + "sct", +] + +[[package]] +name = "rustls-webpki" +version = "0.100.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "261e9e0888cba427c3316e6322805653c9425240b6fd96cee7cb671ab70ab8d0" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "syn" +version = "2.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "ureq" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b11c96ac7ee530603dcdf68ed1557050f374ce55a5a07193ebf8cbc9f8927e9" +dependencies = [ + "base64", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-webpki 0.100.1", + "url", + "webpki-roots", +] + +[[package]] +name = "url" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "web-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338" +dependencies = [ + "rustls-webpki 0.100.1", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3fff2a8 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "dechrome" +version = "0.9.0" +edition = "2021" + +[target.'cfg(windows)'.dependencies] +winreg = "~0.50.0" +is_elevated = "~0.1.2" +ureq = { version = "~2.7.1", features = ["tls"] } + +[profile.release-full] +inherits = "release" +strip = "symbols" +lto = "fat" +codegen-units = 1 +panic = "abort" diff --git a/README.md b/README.md new file mode 100644 index 0000000..9072e48 --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# Dechrome + +Dechrome is a tool written in Rust for batch removing Chromium-based browsers and installing Firefox as a replacement. + +> [!WARNING] +> The script is experimental and wasn't thoroughly tested on all Windows systems. + +> [!NOTE] +> Executing as administrator is preferred in order to remove system-wide installations. Make sure to terminate msedge.exe processes through task manager beforing launching, as the script might fail to delete certain files locked by Edge + +# Reasoning + +Chromium-based browsers hold around 74.05% market share across all devices, 78.58% on desktop, 90.45%* on Windows, according to [GlobalStats statcounter](https://gs.statcounter.com/browser-market-share/desktop/worldwide/#monthly-202307-202307-bar) as of July 2023. +> *Calculated by excluding Safari market share from browser market share on desktop, may be inaccurate + +This causes multiple issues: +1. Increased attack surface +2. Illusion of choice +3. Single entity control + +### Increased attack surface +Having a single product spread across millions of machines imposes a severe risk in case of discovered vulnerability as the area of attack could be worldwide and the patches could take a long time to propagate, giving opportunity for attackers to take advantage of the situation. + +### Illusion of choice +Users are given illusion of choice, no matter what they pick they are likely to end up using Google's product, either [Blink](https://www.chromium.org/blink) or Chromium, since most browsers are based on them and are not advertised as such. + +### Single entity control +While Chromium project is open-source, ultimately Google is in full control of the changes being made to it. Giving away control of the web client to Google, entity controlling most of the web space, gives it the ability to shift and control market to it's will, both client-side and server-side. This provokes monopoly and hurts competition in the long run. See recent [Web Environment Integrity proposal](https://github.com/RupertBenWiser/Web-Environment-Integrity/blob/main/explainer.md). + +# Implemented uninstallers +- Google Chrome +- Google Chrome Canary +- Microsoft Edge +- Brave +- Vivaldi +- Opera +- Opera GX +- Yandex Browser + +# Contributing +All issues and pull requests are welcome! Feel free to open an issue if you've got an idea or a problem. You can open a pull request if you are able to implement it yourself. + +--- +

+ + Made with ponies and love! +
+ GNU GPL © Ryze 2023 +
+

diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..c9094e3 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,30 @@ +use std::io; +use ureq; + +// TODO: Well... +pub type DechromeResult = Result; + +#[derive(Debug)] +pub enum DechromeError { + FileNotFound, + ExecutablePathNotFound, + MismatchedQuotes, + ContentLengthError, + IOError(io::Error), + NetworkError(Box), +} + +impl From for DechromeError { + fn from(err: io::Error) -> Self { + Self::IOError(err) + } +} + +impl From for DechromeError { + fn from(err: ureq::Error) -> Self { + match err { + ureq::Error::Status(code, _) if code == 404 => Self::FileNotFound, + _ => Self::NetworkError(Box::new(err)), + } + } +} diff --git a/src/install.rs b/src/install.rs new file mode 100644 index 0000000..1a3392a --- /dev/null +++ b/src/install.rs @@ -0,0 +1,3 @@ +pub mod firefox; + +pub use firefox::Firefox; diff --git a/src/install/firefox.rs b/src/install/firefox.rs new file mode 100644 index 0000000..52b9a1c --- /dev/null +++ b/src/install/firefox.rs @@ -0,0 +1,41 @@ +use std::{path::Path, process::Command}; +use crate::{PrepareInstaller, Installer, SystemInfo, DechromeError}; +use crate::utils; + +const WIN_ARCH: &str = if cfg!(target_arch = "x86_64") { "win64" } else { "win32" }; + +pub struct PreparedFirefox; +impl Installer for PreparedFirefox { + fn install(self, info: &SystemInfo, installer_path: &Path) -> Result<(), DechromeError> { + let mut command = Command::new(installer_path); + command.arg("/S"); + + if !info.is_elevated { + command.arg("/InstallDirectoryPath".to_owned() + info.local_appdata.to_str().unwrap()); + } + + command.spawn()?.wait()?; + Ok(()) + } +} + +pub struct Firefox; +impl PrepareInstaller for Firefox { + fn prepare(info: &SystemInfo, installer_path: &Path) -> Result { + let urls = &info.preferred_languages.iter() + .map(|lang| format!("https://download.mozilla.org/?product=firefox-latest-ssl&os={WIN_ARCH}&lang={lang}")); + + let res = utils::try_fetch_multiple(urls.clone(), installer_path); + + match res { + Err(DechromeError::FileNotFound) => { + utils::fetch_file(&format!("https://download.mozilla.org/?product=firefox-latest-ssl&os={WIN_ARCH}&lang=en-US"), installer_path)?; + Ok(PreparedFirefox) + }, + Ok(_) => Ok(PreparedFirefox), + Err(v) => Err(v) + } + + } +} + \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..78e79fc --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,9 @@ +pub mod error; +pub mod models; +pub mod utils; + +pub mod install; +pub mod uninstall; + +pub use error::*; +pub use models::*; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..c1912af --- /dev/null +++ b/src/main.rs @@ -0,0 +1,45 @@ +#![windows_subsystem = "windows"] + +use dechrome::{install, uninstall}; +use dechrome::{SystemInfo, PrepareInstaller, Installer, Uninstaller}; + + +fn main() { + let info = SystemInfo::build().unwrap(); + + let mut firefox_path = info.temp.clone(); + firefox_path.push("firefox_installer.exe"); + + println!("Starting..."); + match install::Firefox::prepare(&info, &firefox_path) { + Ok(firefox) => { + println!("Installer downloaded. Removing browsers"); + remove_browsers(&info); + + println!("Installing Firefox"); + let res = firefox.install(&info, &firefox_path); + println!("Result: {res:?}"); + }, + Err(e) => eprintln!("Couldn't download Firefox: {e:?}") + } +} + +fn remove_browsers(info: &SystemInfo) { + println!("Chrome based:"); + for browser in uninstall::get_chrome_based(info) { + let res = browser.uninstall(info); + println!("Result: {res:?}"); + } + + println!("Opera based:"); + for opera in uninstall::get_opera_based(info) { + let res = opera.uninstall(info); + println!("Result: {res:?}"); + } + + if let Some(edge) = uninstall::get_edge_based(info) { + let res = edge.uninstall(info); + println!("Edge: "); + println!("Result: {res:?}"); + } +} diff --git a/src/models.rs b/src/models.rs new file mode 100644 index 0000000..3e404f0 --- /dev/null +++ b/src/models.rs @@ -0,0 +1,146 @@ +use std::env; +use std::io; +use std::path::Path; +use std::path::PathBuf; +use std::process::Child; +use std::process::Command; + +use winreg::enums::HKEY_CURRENT_USER; +use winreg::enums::HKEY_LOCAL_MACHINE; +use winreg::RegKey; + +use is_elevated::is_elevated; + +use crate::utils; +use crate::DechromeError; +use crate::DechromeResult; + +const USER_PROFILE_REG_PATH: &str = r"Control Panel\International\User Profile"; + +pub trait PrepareInstaller { + fn prepare(info: &SystemInfo, installer_path: &Path) -> DechromeResult; +} + +pub trait Installer { + fn install(self, info: &SystemInfo, installer_path: &Path) -> DechromeResult<()>; +} + +pub trait Uninstaller { + fn uninstall(self, info: &SystemInfo) -> DechromeResult<()>; +} + +#[derive(Debug, Hash, PartialEq, Eq)] +pub struct UninstallInfo { + pub display_version: String, + pub invoke_command: String, + pub invoke_args: Vec, +} + +impl UninstallInfo { + pub fn new(display_version: String, invoke_command: String, invoke_args: Vec) -> Self { + Self { + display_version, + invoke_command, + invoke_args, + } + } + + pub fn add_arg(&mut self, arg: &str) { + self.invoke_args.push(arg.to_owned()) + } + + pub fn invoke(self) -> io::Result { + Command::new(self.invoke_command) + .args(self.invoke_args) + .spawn() + } +} + +impl TryFrom for UninstallInfo { + type Error = DechromeError; + + fn try_from(regkey: RegKey) -> Result { + let display_version = regkey.get_value("DisplayVersion")?; + let uninstall_string: String = regkey.get_value("UninstallString")?; + let (invoke_command, invoke_args) = utils::parse_shell(&uninstall_string)?; + + Ok(Self { + display_version, + invoke_command, + invoke_args, + }) + } +} + +#[derive(Debug)] +pub struct SystemInfo { + pub is_elevated: bool, + pub temp: PathBuf, + pub appdata: PathBuf, + pub local_appdata: PathBuf, + pub all_users_data: PathBuf, + pub public: PathBuf, + pub program_files_x86: PathBuf, + pub preferred_languages: Box<[String]>, + pub uninstall_regkeys: Box<[RegKey]>, +} + +impl SystemInfo { + pub fn build() -> io::Result { + let is_elevated = is_elevated(); + let temp = env::temp_dir(); + let appdata = get_env_path("APPDATA"); + let local_appdata = get_env_path("LOCALAPPDATA"); + let program_files_x86 = get_env_path("PROGRAMFILES(X86)"); + let all_users_data = get_env_path("ALLUSERSPROFILE"); + let public = get_env_path("PUBLIC"); + let preferred_languages = get_preferred_languages()?; + let uninstall_regkeys = get_uninstall_regkeys(); + + Ok(Self { + is_elevated, + temp, + appdata, + local_appdata, + all_users_data, + public, + program_files_x86, + preferred_languages, + uninstall_regkeys, + }) + } +} + +fn get_preferred_languages() -> io::Result> { + let hkcu = RegKey::predef(HKEY_CURRENT_USER); + let desktop_key = hkcu.open_subkey(USER_PROFILE_REG_PATH)?; + let langs: Vec = desktop_key.get_value("Languages")?; + + Ok(langs.into()) +} + +fn get_env_path(var: &str) -> PathBuf { + env::var(var) + .expect("Variable doesn't exist") + .into() +} + +fn get_uninstall_regkeys() -> Box<[RegKey]> { + let base_keys = [ + RegKey::predef(HKEY_CURRENT_USER), + RegKey::predef(HKEY_LOCAL_MACHINE), + ]; + + let bit_prefixes = [r"", r"WOW6432Node"]; + + let combinations = base_keys + .iter() + .flat_map(|base_key| bit_prefixes.iter().map(move |prefix| (base_key, prefix))); + + let regkeys = combinations.filter_map(|(base_key, prefix)| { + let path = format!(r"SOFTWARE\{prefix}\Microsoft\Windows\CurrentVersion\Uninstall"); + base_key.open_subkey(path).ok() + }); + + regkeys.collect() +} diff --git a/src/uninstall.rs b/src/uninstall.rs new file mode 100644 index 0000000..c83bf2d --- /dev/null +++ b/src/uninstall.rs @@ -0,0 +1,27 @@ +pub mod chrome_based; +pub mod opera_based; +pub mod edge_based; + +pub use chrome_based::get_chrome_based; +pub use opera_based::get_opera_based; +pub use edge_based::get_edge_based; + +use crate::{DechromeResult, SystemInfo, UninstallInfo, Uninstaller}; + +#[derive(Debug)] +pub struct SimpleUninstaller { + uninstall_info: UninstallInfo, +} + +impl SimpleUninstaller { + pub fn new(uninstall_info: UninstallInfo) -> Self { + Self { uninstall_info } + } +} + +impl Uninstaller for SimpleUninstaller { + fn uninstall(self, _info: &SystemInfo) -> DechromeResult<()> { + self.uninstall_info.invoke()?; + Ok(()) + } +} diff --git a/src/uninstall/chrome_based.rs b/src/uninstall/chrome_based.rs new file mode 100644 index 0000000..d0b8441 --- /dev/null +++ b/src/uninstall/chrome_based.rs @@ -0,0 +1,25 @@ +use crate::uninstall::SimpleUninstaller; +use crate::utils; +use crate::SystemInfo; + +const BROWSERS: [&str; 5] = [ + "Google Chrome", + "Google Chrome SxS", + "BraveSoftware Brave-Browser", + "Vivaldi", + "YandexBrowser", +]; + +// Get uninstallers based on Chrome installer, which have an option --force-uninstall for silent execution +pub fn get_chrome_based(info: &SystemInfo) -> Vec { + let uninstall_info = BROWSERS + .iter() + .flat_map(|browser| utils::find_browser_uninstall_info(info, browser)); + + let uninstallers = uninstall_info.map(|mut info| { + info.add_arg("--force-uninstall"); + SimpleUninstaller::new(info) + }); + + uninstallers.collect() +} diff --git a/src/uninstall/edge_based.rs b/src/uninstall/edge_based.rs new file mode 100644 index 0000000..6a54d7c --- /dev/null +++ b/src/uninstall/edge_based.rs @@ -0,0 +1,78 @@ +use winreg::{RegKey, enums::HKEY_LOCAL_MACHINE}; +use std::fs; + +use crate::{Uninstaller, DechromeResult, SystemInfo}; + +const EDGE_PATHS: [&str; 3] = [ + "Microsoft/Edge", + "Microsoft/EdgeCore", + "Microsoft/EdgeUpdate", +]; + +const EDGE_REGKEYS: [&str; 3] = [ + r"SOFTWARE\Microsoft\Active Setup\Installed Components\{9459C573-B17A-45AE-9F64-1857B5D58CEE}", + r"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\Microsoft Edge", + r"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\Microsoft Edge Update" +]; + +const EDGE_UPDATE_REGKEY: &str = r"SOFTWARE\Microsoft\EdgeUpdate"; + + +pub struct EdgeUninstaller; + +#[allow(unused_must_use)] // Supress warnings of for io::Result +impl EdgeUninstaller { + fn remove_files(info: &SystemInfo) { + let program_files_x86 = &info.program_files_x86; + + for path in EDGE_PATHS { + let mut full_path = program_files_x86.clone(); + full_path.push(path); + + fs::remove_dir_all(&full_path); + } + + let mut link_path = info.public.clone(); + link_path.push("Desktop/Microsoft Edge.lnk"); + fs::remove_file(link_path); + + let mut link_path = info.all_users_data.clone(); + link_path.push("Microsoft/Windows/Start Menu/Programs/Microsoft Edge.lnk"); + fs::remove_file(link_path); + let mut link_path = info.appdata.clone(); + link_path.push("Microsoft/Internet Explorer/Quick Launch/User Pinned/TaskBar/Microsoft Edge.lnk"); + fs::remove_file(link_path); + } + + fn remove_regkeys() { + let hlkm = RegKey::predef(HKEY_LOCAL_MACHINE); + + for path in EDGE_REGKEYS { + hlkm.delete_subkey_all(path); + } + } + + fn disable_update() -> DechromeResult<()> { + let hlkm = RegKey::predef(HKEY_LOCAL_MACHINE); + let (edge_update_key, _) = hlkm.create_subkey(EDGE_UPDATE_REGKEY)?; + edge_update_key.set_value("DoNotUpdateToEdgeWithChromium", &1u32)?; // DWORD is u32 + + Ok(()) + } +} + +impl Uninstaller for EdgeUninstaller { + fn uninstall(self, info: &SystemInfo) -> DechromeResult<()> { + Self::remove_files(info); + Self::remove_regkeys(); + Self::disable_update() + } +} + +pub fn get_edge_based(info: &SystemInfo) -> Option { + if info.is_elevated { + Some(EdgeUninstaller {}) + } else { + None + } +} diff --git a/src/uninstall/opera_based.rs b/src/uninstall/opera_based.rs new file mode 100644 index 0000000..8e292d4 --- /dev/null +++ b/src/uninstall/opera_based.rs @@ -0,0 +1,48 @@ +use std::collections::HashSet; + +use crate::utils; +use crate::{DechromeResult, SystemInfo, UninstallInfo, Uninstaller}; + +const BROWSERS: [&str; 2] = ["Opera", "Opera GX"]; + +#[derive(Hash, PartialEq, Eq)] +pub struct OperaUninstaller { + invoke_info: UninstallInfo, +} + +impl OperaUninstaller { + pub fn new(invoke_info: UninstallInfo) -> Self { + Self { invoke_info } + } +} + +impl Uninstaller for OperaUninstaller { + fn uninstall(self, _: &SystemInfo) -> DechromeResult<()> { + let mut uninstaller = self.invoke_info.invoke()?; + uninstaller.wait()?; // Wait for the installer to close, can't allow multiple of them to run + + Ok(()) + } +} + +pub fn get_opera_based(info: &SystemInfo) -> HashSet { + let uninstall_info = BROWSERS + .iter() + .flat_map(|browser| utils::find_browser_uninstall_info_starting_with(info, browser)); + + let uninstallers = uninstall_info.map(|mut info| { + let version = &info.display_version; + + // It's capitalised in the registry, but the actual filename isn't + // Blame Opera + let uninstaller_dir = info.invoke_command.trim_end_matches("Launcher.exe"); + let new_command = format!("{uninstaller_dir}/{version}/installer.exe"); + + info.invoke_command = new_command; + info.add_arg("--runimmediately"); // Unattended execution + + OperaUninstaller::new(info) + }); + + uninstallers.collect() +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..50ce5c8 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,103 @@ +use std::{fs::File, io::Write, path::Path}; +use ureq; + +use crate::{DechromeError, DechromeResult, SystemInfo, UninstallInfo}; + +pub fn fetch_file(url: &str, path: &Path) -> DechromeResult<()> { + if path.exists() { + return Ok(()); + } + + let response = ureq::get(url).call()?; + let length = response + .header("Content-Length") + .ok_or(DechromeError::ContentLengthError)?; + let length = length + .parse() + .map_err(|_| DechromeError::ContentLengthError)?; + + let mut buffer = Vec::with_capacity(length); + response.into_reader().read_to_end(&mut buffer)?; + + let mut file = File::create(path)?; + file.write_all(&buffer)?; + + Ok(()) +} + +pub fn try_fetch_multiple>( + urls: I, + path: &Path, +) -> DechromeResult<()> { + for url in urls { + let res = fetch_file(&url, path); + + if let Err(DechromeError::FileNotFound) = res { + continue; + } + + if res.is_ok() { + return Ok(()); + } + + res?; + } + + Err(DechromeError::FileNotFound) +} + +pub fn parse_shell(input: &str) -> DechromeResult<(String, Vec)> { + let mut in_quotes = false; + + let input = input.trim(); + let input_split = input.split(|char| match char { + '"' => { + in_quotes = !in_quotes; + false + } + ' ' => !in_quotes, + _ => false, + }); + + let mut parsed = input_split.map(|split| split.replace('"', "")); + let command = parsed.next().ok_or(DechromeError::ExecutablePathNotFound)?; + let args = parsed.collect(); + + if in_quotes { + // Has to run after .collect because that's when everything runs + return Err(DechromeError::MismatchedQuotes); + } + + Ok((command, args)) +} + +pub fn find_browser_uninstall_info<'a>( + info: &'a SystemInfo, + browser: &'a str, +) -> impl Iterator + 'a { + let regkeys = info + .uninstall_regkeys + .iter() + .filter_map(move |regkey| regkey.open_subkey(browser).ok()); + + regkeys.filter_map(|browser_key| { + UninstallInfo::try_from(browser_key).ok() + }) +} + +pub fn find_browser_uninstall_info_starting_with<'a>( + info: &'a SystemInfo, + starts_with: &'a str, +) -> impl Iterator + 'a { + let regkeys = info.uninstall_regkeys.iter().flat_map(move |regkey| { + regkey + .enum_keys() + .filter_map(|res| res.ok()) // Filter out successful ones + .filter(move |regkey| regkey.starts_with(starts_with)) // Get only matched + .filter_map(|regname| regkey.open_subkey(regname).ok()) // Try to open them + }); + + regkeys.filter_map(|browser_key| { + UninstallInfo::try_from(browser_key).ok() + }) +}