├── .gitignore ├── shared ├── .gitignore ├── build.rs ├── Cargo.toml ├── schema │ └── spotify-ad-guard.capnp ├── src │ └── lib.rs └── Cargo.lock ├── burnt-sushi ├── .gitignore ├── icon.ico ├── src │ ├── logger │ │ ├── console │ │ │ ├── mod.rs │ │ │ ├── raw.rs │ │ │ └── log.rs │ │ ├── traits.rs │ │ ├── mod.rs │ │ ├── noop.rs │ │ ├── global.rs │ │ └── file.rs │ ├── named_mutex.rs │ ├── args.rs │ ├── tray.rs │ ├── rpc.rs │ ├── resolver.rs │ ├── blocker.rs │ ├── main.rs │ ├── update.rs │ └── spotify_process_scanner.rs ├── BurntSushi.exe.manifest ├── .vscode │ └── launch.json ├── build.rs ├── Cargo.toml └── wix │ └── main.wxs ├── burnt-sushi-blocker ├── .gitignore ├── src │ ├── utils.rs │ ├── filters.rs │ ├── hooks.rs │ └── lib.rs ├── Cargo.toml └── Cargo.lock ├── icon.png ├── sync-filters.ps1 ├── run-cmd.ps1 ├── .github └── workflows │ └── build.yml ├── LICENSE ├── .vscode └── launch.json ├── README.md └── filter.toml /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /shared/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /burnt-sushi/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /burnt-sushi-blocker/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenByteDev/burnt-sushi/HEAD/icon.png -------------------------------------------------------------------------------- /burnt-sushi/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenByteDev/burnt-sushi/HEAD/burnt-sushi/icon.ico -------------------------------------------------------------------------------- /burnt-sushi/src/logger/console/mod.rs: -------------------------------------------------------------------------------- 1 | #[allow(dead_code)] 2 | pub mod raw; 3 | 4 | mod log; 5 | pub use self::log::*; 6 | -------------------------------------------------------------------------------- /burnt-sushi/src/logger/traits.rs: -------------------------------------------------------------------------------- 1 | use std::{any::Any, fmt::Debug}; 2 | 3 | pub trait SimpleLog: Any + Debug + Send + Sync { 4 | fn log(&mut self, message: &str); 5 | } 6 | -------------------------------------------------------------------------------- /burnt-sushi/src/logger/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod console; 2 | pub mod file; 3 | pub mod global; 4 | pub mod noop; 5 | 6 | mod traits; 7 | 8 | pub use console::Console; 9 | pub use file::FileLog; 10 | pub use traits::*; 11 | -------------------------------------------------------------------------------- /burnt-sushi/src/logger/noop.rs: -------------------------------------------------------------------------------- 1 | use super::SimpleLog; 2 | 3 | #[derive(Debug)] 4 | #[allow(unused)] 5 | pub struct NoopLog; 6 | 7 | impl SimpleLog for NoopLog { 8 | fn log(&mut self, _message: &str) {} 9 | } 10 | -------------------------------------------------------------------------------- /shared/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | cargo_emit::rerun_if_changed!("schema\\spotify-ad-guard.capnp"); 3 | capnpc::CompilerCommand::new() 4 | .src_prefix("schema") 5 | .file("schema\\spotify-ad-guard.capnp") 6 | .run() 7 | .unwrap(); 8 | } 9 | -------------------------------------------------------------------------------- /sync-filters.ps1: -------------------------------------------------------------------------------- 1 | curl.exe --url https://raw.githubusercontent.com/abba23/spotify-adblock/main/config.toml --output .\filter.toml 2 | Set-Content -Path .\filter.toml -Value ("# source: https://github.com/abba23/spotify-adblock/blob/main/config.toml`n`n" + (Get-Content .\filter.toml -Raw)) 3 | -------------------------------------------------------------------------------- /burnt-sushi-blocker/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::any::Any; 2 | 3 | pub fn panic_info_to_string(info: Box) -> String { 4 | if let Some(s) = info.downcast_ref::<&str>() { 5 | s.to_string() 6 | } else if let Ok(s) = info.downcast::() { 7 | *s 8 | } else { 9 | "Unknown panic".to_string() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /burnt-sushi/BurntSushi.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /shared/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shared" 3 | version = "0.3.2" 4 | description = "Spotify AdBlocker for Windows" 5 | repository = "https://github.com/OpenByteDev/burnt-sushi" 6 | license = "MIT" 7 | authors = ["OpenByte "] 8 | edition = "2024" 9 | keywords = ["spotify", "adblocker", "windows", "blocker", "payload"] 10 | 11 | [dependencies] 12 | capnp = { version = "0.23.0", features = ["alloc"], default-features = false } 13 | regex = { version = "1.10.5", features = ["std"], default-features = false } 14 | enum-map = { version = "2.7.3", default-features = false } 15 | 16 | [build-dependencies] 17 | capnpc = "0.23.0" 18 | cargo-emit = "0.2.1" 19 | 20 | [profile.release] 21 | strip = true 22 | lto = true 23 | opt-level = 3 24 | codegen-units = 1 25 | -------------------------------------------------------------------------------- /shared/schema/spotify-ad-guard.capnp: -------------------------------------------------------------------------------- 1 | @0xaff784be6017f80e; 2 | 3 | interface BlockerService { 4 | registerLogger @0 (logger :Logger); 5 | setRuleset @1 (hook :FilterHook, ruleset :FilterRuleset); 6 | enableFiltering @2 (); 7 | disableFiltering @3 (); 8 | 9 | enum FilterHook { 10 | getAddrInfo @0; 11 | cefUrlRequestCreate @1; 12 | } 13 | 14 | struct FilterRuleset { 15 | whitelist @0 :List(Text); 16 | blacklist @1 :List(Text); 17 | } 18 | 19 | interface Logger { 20 | struct Request { 21 | url @0 :Text; 22 | hook @1 :FilterHook; 23 | blocked @2 :Bool; 24 | } 25 | 26 | logRequest @0 (request :Request); 27 | logMessage @1 (message :Text); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /run-cmd.ps1: -------------------------------------------------------------------------------- 1 | param ([string] $command) 2 | 3 | $PSNativeCommandUseErrorActionPreference = $true 4 | $ErrorActionPreference = 'Stop' 5 | 6 | $IncludeTarget = $true 7 | if ($command -eq "fmt") { 8 | $IncludeTarget = $false 9 | } 10 | 11 | cargo +nightly $command --manifest-path=shared/Cargo.toml $(if ($IncludeTarget) { "--target" } else { "" }) $(if ($IncludeTarget) { "i686-pc-windows-msvc" } else { "" }) $args 12 | cargo +nightly $command --manifest-path=shared/Cargo.toml $(if ($IncludeTarget) { "--target" } else { "" }) $(if ($IncludeTarget) { "x86_64-pc-windows-msvc" } else { "" }) $args 13 | cargo +nightly $command --manifest-path=burnt-sushi-blocker/Cargo.toml $(if ($IncludeTarget) { "--target" } else { "" }) $(if ($IncludeTarget) { "i686-pc-windows-msvc" } else { "" }) $args 14 | cargo +nightly $command --manifest-path=burnt-sushi/Cargo.toml $(if ($IncludeTarget) { "--target" } else { "" }) $(if ($IncludeTarget) { "x86_64-pc-windows-msvc" } else { "" }) $args 15 | 16 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: windows-latest 15 | strategy: 16 | matrix: 17 | target: [i686-pc-windows-msvc, x86_64-pc-windows-msvc] 18 | steps: 19 | - uses: actions/checkout@v6 20 | 21 | - name: Install nightly for ${{ matrix.target }} 22 | uses: dtolnay/rust-toolchain@nightly 23 | with: 24 | targets: ${{ matrix.target }} 25 | 26 | - name: Install Cap'n Proto 27 | run: choco install capnproto 28 | 29 | - name: Build shared 30 | run: cargo build --manifest-path=shared/Cargo.toml --target ${{ matrix.target }} 31 | 32 | - name: Build burnt-sushi-blocker 33 | run: cargo build --manifest-path=burnt-sushi-blocker/Cargo.toml --target ${{ matrix.target }} 34 | 35 | - name: Build burnt-sushi 36 | run: cargo build --manifest-path=burnt-sushi/Cargo.toml --target ${{ matrix.target }} 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) OpenByte 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /burnt-sushi-blocker/src/filters.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use arc_swap::ArcSwap; 4 | use enum_map::EnumMap; 5 | use regex::RegexSet; 6 | 7 | pub type FilterHook = shared::rpc::blocker_service::FilterHook; 8 | 9 | #[derive(Clone, Debug)] 10 | pub struct Filters { 11 | rulesets: Arc>>, 12 | } 13 | 14 | impl Filters { 15 | #[must_use] 16 | pub fn empty() -> Self { 17 | Self { 18 | rulesets: Arc::new(EnumMap::default()), 19 | } 20 | } 21 | 22 | pub fn replace_ruleset(&self, hook: FilterHook, ruleset: FilterRuleset) { 23 | self.rulesets[hook].store(Arc::new(ruleset)); 24 | } 25 | 26 | #[must_use] 27 | pub fn check(&self, hook: FilterHook, request: &str) -> bool { 28 | let ruleset = self.rulesets[hook].load(); 29 | ruleset.check(request) 30 | } 31 | } 32 | 33 | #[derive(Debug, Clone, Default)] 34 | pub struct FilterRuleset { 35 | pub whitelist: RegexSet, 36 | pub blacklist: RegexSet, 37 | } 38 | 39 | impl FilterRuleset { 40 | fn check(&self, request: &str) -> bool { 41 | (self.whitelist.is_empty() || self.whitelist.is_match(request)) 42 | && !self.blacklist.is_match(request) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /burnt-sushi-blocker/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "burnt-sushi-blocker" 3 | version = "0.3.2" 4 | description = "Spotify AdBlocker for Windows" 5 | repository = "https://github.com/OpenByteDev/burnt-sushi" 6 | license = "MIT" 7 | authors = ["OpenByte "] 8 | edition = "2024" 9 | keywords = ["spotify", "adblocker", "windows", "blocker", "payload"] 10 | 11 | [dependencies] 12 | dll-syringe = { version = "0.17.0", features = ["payload-utils"], default-features = true } 13 | capnp = { version = "0.23.0", features = ["alloc"], default-features = false } 14 | capnp-rpc = { version = "0.23.0", default-features = false } 15 | futures = { version = "0.3.30", default-features = false } 16 | tokio = { version = "1.38.1", features = ["net", "rt", "macros", "sync"], default-features = false } 17 | tokio-util = { version = "0.7.11", features = ["compat"], default-features = false } 18 | winapi = { version = "0.3.9", features = ["ws2tcpip", "rpc"], default-features = false } 19 | retour = { version = "0.4.0-alpha.4", features = ["nightly", "static-detour"], default-features = false } 20 | shared = { path = "../shared", default-features = false } 21 | regex = { version = "1.10.5", default-features = false } 22 | enum-map = { version = "2.7.3", default-features = false } 23 | arc-swap = { version = "1.7.1", default-features = false } 24 | 25 | [lib] 26 | crate-type = ["cdylib"] 27 | 28 | [profile.release] 29 | strip = true 30 | lto = true 31 | opt-level = 3 32 | codegen-units = 1 33 | -------------------------------------------------------------------------------- /burnt-sushi/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug executable 'BurntSushi'", 11 | "cargo": { 12 | "args": [ 13 | "build", 14 | "--bin=BurntSushi", 15 | "--package=burnt-sushi" 16 | ], 17 | "filter": { 18 | "name": "BurntSushi", 19 | "kind": "bin" 20 | } 21 | }, 22 | "args": [], 23 | "cwd": "${workspaceFolder}" 24 | }, 25 | { 26 | "type": "lldb", 27 | "request": "launch", 28 | "name": "Debug unit tests in executable 'BurntSushi'", 29 | "cargo": { 30 | "args": [ 31 | "test", 32 | "--no-run", 33 | "--bin=BurntSushi", 34 | "--package=burnt-sushi" 35 | ], 36 | "filter": { 37 | "name": "BurntSushi", 38 | "kind": "bin" 39 | } 40 | }, 41 | "args": [], 42 | "cwd": "${workspaceFolder}" 43 | } 44 | ] 45 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug executable 'spotify-ad-guard'", 11 | "cargo": { 12 | "args": [ 13 | "build", 14 | "--bin=spotify-ad-guard", 15 | "--package=spotify-ad-guard" 16 | ], 17 | "filter": { 18 | "name": "spotify-ad-guard", 19 | "kind": "bin" 20 | } 21 | }, 22 | "args": [], 23 | "cwd": "${workspaceFolder}" 24 | }, 25 | { 26 | "type": "lldb", 27 | "request": "launch", 28 | "name": "Debug unit tests in executable 'spotify-ad-guard'", 29 | "cargo": { 30 | "args": [ 31 | "test", 32 | "--no-run", 33 | "--bin=spotify-ad-guard", 34 | "--package=spotify-ad-guard" 35 | ], 36 | "filter": { 37 | "name": "spotify-ad-guard", 38 | "kind": "bin" 39 | } 40 | }, 41 | "args": [], 42 | "cwd": "${workspaceFolder}" 43 | } 44 | ] 45 | } -------------------------------------------------------------------------------- /shared/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(variant_count)] 2 | #![warn(clippy::pedantic)] 3 | 4 | use core::{fmt, hash}; 5 | use std::mem; 6 | 7 | #[allow(dead_code)] 8 | #[allow(clippy::pedantic)] 9 | mod spotify_ad_guard_capnp { 10 | include!(concat!(env!("OUT_DIR"), "\\spotify_ad_guard_capnp.rs")); 11 | } 12 | 13 | pub mod rpc { 14 | pub use super::spotify_ad_guard_capnp::*; 15 | } 16 | 17 | #[allow(clippy::derived_hash_with_manual_eq)] 18 | impl hash::Hash for rpc::blocker_service::FilterHook { 19 | fn hash(&self, state: &mut H) { 20 | core::mem::discriminant(self).hash(state); 21 | } 22 | } 23 | 24 | impl fmt::Display for rpc::blocker_service::FilterHook { 25 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 26 | match self { 27 | rpc::blocker_service::FilterHook::GetAddrInfo => { 28 | write!(f, "getaddrinfo") 29 | } 30 | rpc::blocker_service::FilterHook::CefUrlRequestCreate => { 31 | write!(f, "cef_urlrequest_create") 32 | } 33 | } 34 | } 35 | } 36 | 37 | impl enum_map::Enum for rpc::blocker_service::FilterHook { 38 | const LENGTH: usize = mem::variant_count::(); 39 | 40 | fn from_usize(value: usize) -> Self { 41 | match value { 42 | 0 => rpc::blocker_service::FilterHook::GetAddrInfo, 43 | 1 => rpc::blocker_service::FilterHook::CefUrlRequestCreate, 44 | _ => unreachable!(), 45 | } 46 | } 47 | 48 | fn into_usize(self) -> usize { 49 | match self { 50 | rpc::blocker_service::FilterHook::GetAddrInfo => 0, 51 | rpc::blocker_service::FilterHook::CefUrlRequestCreate => 1, 52 | } 53 | } 54 | } 55 | 56 | impl enum_map::EnumArray for rpc::blocker_service::FilterHook { 57 | type Array = [T; mem::variant_count::()]; 58 | } 59 | -------------------------------------------------------------------------------- /burnt-sushi/src/logger/global.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::Debug, 3 | sync::{Mutex, MutexGuard}, 4 | }; 5 | 6 | use chrono::Local; 7 | 8 | use log::Log; 9 | 10 | use crate::APP_NAME; 11 | 12 | use super::{Console, FileLog, SimpleLog}; 13 | 14 | static LOGGER: GlobalLoggerHolder = GlobalLoggerHolder(Mutex::new(GlobalLogger::new())); 15 | 16 | pub fn init() -> &'static GlobalLoggerHolder { 17 | let _ = log::set_logger(&LOGGER); 18 | &LOGGER 19 | } 20 | 21 | pub fn get() -> MutexGuard<'static, GlobalLogger> { 22 | LOGGER.0.lock().unwrap() 23 | } 24 | 25 | pub fn unset() { 26 | let mut logger = get(); 27 | logger.console = None; 28 | logger.file = None; 29 | } 30 | 31 | #[derive(Debug)] 32 | pub struct GlobalLoggerHolder(Mutex); 33 | 34 | #[derive(Debug)] 35 | pub struct GlobalLogger { 36 | pub console: Option, 37 | pub file: Option, 38 | } 39 | 40 | impl GlobalLogger { 41 | pub const fn new() -> Self { 42 | GlobalLogger { 43 | console: None, 44 | file: None, 45 | } 46 | } 47 | } 48 | 49 | impl Log for GlobalLoggerHolder { 50 | fn enabled(&self, _metadata: &log::Metadata) -> bool { 51 | true 52 | } 53 | 54 | fn log(&self, record: &log::Record) { 55 | if !record.target().starts_with(APP_NAME) { 56 | return; 57 | } 58 | 59 | let mut logger = self.0.lock().unwrap(); 60 | if let Some(log) = &mut logger.console { 61 | let message = format!("[{}] {}", record.level(), record.args()); 62 | log.log(&message); 63 | } 64 | if let Some(log) = &mut logger.file { 65 | let date_time = Local::now().format("%Y-%m-%d %H:%M:%S"); 66 | let message = format!("{} [{}] {}", date_time, record.level(), record.args()); 67 | log.log(&message); 68 | } 69 | } 70 | 71 | fn flush(&self) {} 72 | } 73 | -------------------------------------------------------------------------------- /burnt-sushi/src/logger/console/raw.rs: -------------------------------------------------------------------------------- 1 | // Modified from https://github.com/Freaky/Compactor/blob/67a72255ee4e72ff86224cf812a4c8ea07f885a6/src/console.rs 2 | 3 | // Helper functions for handling the Windows console from a GUI context. 4 | // 5 | // Windows subsystem applications must explicitly attach to an existing console 6 | // before stdio works, and if not available, create their own if they wish to 7 | // print anything. 8 | // 9 | // These functions enable that, primarily for the purposes of displaying Rust 10 | // panics. 11 | 12 | use winapi::um::consoleapi::AllocConsole; 13 | use winapi::um::wincon::{ATTACH_PARENT_PROCESS, AttachConsole, FreeConsole, GetConsoleWindow}; 14 | use winapi::um::winuser::SW_HIDE; 15 | use winapi::um::winuser::SW_SHOW; 16 | use winapi::um::winuser::ShowWindow; 17 | 18 | /// Check if we're attached to an existing Windows console 19 | pub fn is_attached() -> bool { 20 | unsafe { !GetConsoleWindow().is_null() } 21 | } 22 | 23 | /// Try to attach to an existing Windows console, if necessary. 24 | /// 25 | /// It's normally a no-brainer to call this - it just makes info! and friends 26 | /// work as expected, without cluttering the screen with a console in the general 27 | /// case. 28 | pub fn attach() -> bool { 29 | if is_attached() { 30 | return true; 31 | } 32 | 33 | unsafe { AttachConsole(ATTACH_PARENT_PROCESS) != 0 } 34 | } 35 | 36 | /// Try to allocate ourselves a new console. 37 | pub fn alloc() -> bool { 38 | unsafe { AllocConsole() != 0 } 39 | } 40 | 41 | /// Free any allocated console, if any. 42 | pub fn free() { 43 | unsafe { FreeConsole() }; 44 | } 45 | 46 | pub fn showhide_console(show: bool) { 47 | let hwnd = unsafe { GetConsoleWindow() }; 48 | if !hwnd.is_null() { 49 | unsafe { 50 | ShowWindow(hwnd, if show { SW_SHOW } else { SW_HIDE }); 51 | } 52 | } 53 | } 54 | 55 | pub fn show_console() { 56 | showhide_console(true); 57 | } 58 | pub fn hide_console() { 59 | showhide_console(false); 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BurntSushi 2 | 3 | [![Build](https://github.com/OpenByteDev/burnt-sushi/actions/workflows/build.yml/badge.svg)](https://github.com/OpenByteDev/burnt-sushi/actions/workflows/build.yml) [![Last Release](https://img.shields.io/github/v/release/OpenByteDev/burnt-sushi?include_prereleases)](https://github.com/OpenByteDev/burnt-sushi/releases/latest/) [![License](https://img.shields.io/github/license/OpenByteDev/burnt-sushi)](https://github.com/OpenByteDev/burnt-sushi/blob/master/LICENSE) 4 | 5 | A Spotify AdBlocker for Windows that works via DLL injection and function hooking. 6 | 7 | ## Installation 8 | The latest version can be downloaded [here](https://github.com/OpenByteDev/burnt-sushi/releases/latest). Both a portable and an installed version is available. 9 | 10 | ## FAQ 11 | ### How does it work? 12 | BurntSushi works by intercepting network requests and blocking ones that match a set of [filters](https://github.com/OpenByteDev/burnt-sushi/blob/master/filter.toml). This is implemented by injecting a dynamic library into the Spotify process that overrides [`getaddrinfo`](https://docs.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfo) from the Windows API and `cef_urlrequest_create` from [libcef](https://github.com/chromiumembedded/cef). 13 | The status of the Spotify process is determined using [`wineventhook`](https://github.com/OpenByteDev/wineventhook-rs) which is based on [`SetWinEventHook`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwineventhook). 14 | 15 | ### Can this be detected by Spotify? 16 | Theoretically yes, but practically it probably won't. 17 | 18 | ### Does it work on Linux? 19 | No, BurntSushi supports Windows only, but you can check out [spotify-adblock](https://github.com/abba23/spotify-adblock) instead. 20 | 21 | ## Credits 22 | Inspired by https://github.com/abba23/spotify-adblock 23 | 24 | Original icon made by [Freepik](https://www.freepik.com/) from [flaticon.com](https://www.flaticon.com/) 25 | -------------------------------------------------------------------------------- /burnt-sushi/build.rs: -------------------------------------------------------------------------------- 1 | use std::{env, fs, path::PathBuf, process::Command}; 2 | 3 | fn main() { 4 | let mut res = winres::WindowsResource::new(); 5 | res.set_language(0x0409 /* English */); 6 | res.set_icon("icon.ico"); 7 | res.set_icon_with_id("icon.ico", "TRAYICON"); 8 | res.set_manifest_file("BurntSushi.exe.manifest"); 9 | res.set("FileDescription", env!("CARGO_PKG_DESCRIPTION")); 10 | res.set("ProductName", "BurntSushi"); 11 | res.set("OriginalFilename", "BurntSushi.exe"); 12 | res.set("CompanyName", "OpenByte"); 13 | res.compile().unwrap(); 14 | 15 | fs::copy( 16 | build_crate( 17 | "burnt-sushi-blocker", 18 | "x86_64-pc-windows-msvc", 19 | "burnt_sushi_blocker.dll", 20 | ), 21 | PathBuf::from(env::var_os("OUT_DIR").unwrap()).join("BurntSushiBlocker_x64.dll"), 22 | ) 23 | .unwrap(); 24 | 25 | let mut source_config_path = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); 26 | source_config_path.push(".."); 27 | source_config_path.push("filter.toml"); 28 | 29 | let mut target_config_path = PathBuf::from(env::var_os("OUT_DIR").unwrap()); 30 | target_config_path.push("filter.toml"); 31 | fs::copy(source_config_path, target_config_path).unwrap(); 32 | } 33 | 34 | fn build_crate(name: &str, target: &str, file: &str) -> PathBuf { 35 | // TODO: use encargo 36 | let cargo_exe = PathBuf::from(env::var_os("CARGO").unwrap()); 37 | let is_release = env::var("PROFILE").unwrap().eq_ignore_ascii_case("release"); 38 | let mut crate_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); 39 | crate_dir.push(".."); 40 | crate_dir.push(name); 41 | 42 | let mut command = Command::new(cargo_exe); 43 | 44 | command 45 | .arg("build") 46 | .arg("--target") 47 | .arg(target) 48 | .current_dir(&crate_dir); 49 | 50 | if is_release { 51 | command.arg("--release"); 52 | } 53 | 54 | let status = command.spawn().unwrap().wait().unwrap(); 55 | assert!(status.success()); 56 | 57 | let mut crate_artifact = crate_dir; 58 | crate_artifact.push("target"); 59 | crate_artifact.push(target); 60 | crate_artifact.push(if is_release { "release" } else { "debug" }); 61 | crate_artifact.push(file); 62 | 63 | assert!(crate_artifact.exists(), "{crate_artifact:?}"); 64 | 65 | crate_artifact 66 | } 67 | -------------------------------------------------------------------------------- /burnt-sushi/src/named_mutex.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use std::{io, marker::PhantomData, os::windows::raw::HANDLE, ptr}; 4 | 5 | use widestring::U16CString; 6 | use winapi::{ 7 | shared::winerror::WAIT_TIMEOUT, 8 | um::{ 9 | synchapi::{CreateMutexW, ReleaseMutex, WaitForSingleObject}, 10 | winbase::{INFINITE, WAIT_ABANDONED, WAIT_OBJECT_0}, 11 | }, 12 | }; 13 | 14 | #[derive(Debug)] 15 | pub struct NamedMutex(HANDLE); 16 | 17 | impl NamedMutex { 18 | pub fn new(name: &str) -> io::Result { 19 | let name = U16CString::from_str(format!("Global\\{}", &name)).unwrap(); 20 | 21 | let handle = unsafe { CreateMutexW(ptr::null_mut(), 0, name.as_ptr()) }; 22 | 23 | if handle.is_null() { 24 | Err(io::Error::last_os_error()) 25 | } else { 26 | Ok(Self(handle)) 27 | } 28 | } 29 | 30 | pub fn try_lock(&'_ self) -> io::Result>> { 31 | let rc = unsafe { WaitForSingleObject(self.0, 0) }; 32 | 33 | if rc == WAIT_OBJECT_0 || rc == WAIT_ABANDONED { 34 | Ok(Some(unsafe { self.new_guard() })) 35 | } else if rc == WAIT_TIMEOUT { 36 | Ok(None) 37 | } else { 38 | Err(io::Error::last_os_error()) 39 | } 40 | } 41 | 42 | pub fn lock(&'_ self) -> io::Result> { 43 | let rc = unsafe { WaitForSingleObject(self.0, INFINITE) }; 44 | 45 | if rc == WAIT_OBJECT_0 || rc == WAIT_ABANDONED { 46 | Ok(unsafe { self.new_guard() }) 47 | } else { 48 | Err(io::Error::last_os_error()) 49 | } 50 | } 51 | 52 | pub unsafe fn force_unlock(&self) -> io::Result<()> { 53 | unsafe { self.new_guard() }.unlock() 54 | } 55 | 56 | unsafe fn new_guard(&'_ self) -> NamedMutexGuard<'_> { 57 | NamedMutexGuard(self.0, PhantomData) 58 | } 59 | } 60 | 61 | #[derive(Debug)] 62 | pub struct NamedMutexGuard<'lock>(HANDLE, PhantomData<&'lock NamedMutex>); 63 | 64 | impl NamedMutexGuard<'_> { 65 | pub fn unlock(mut self) -> io::Result<()> { 66 | unsafe { self.unlock_core() }?; 67 | self.0 = ptr::null_mut(); 68 | Ok(()) 69 | } 70 | 71 | unsafe fn unlock_core(&mut self) -> io::Result<()> { 72 | let result = unsafe { ReleaseMutex(self.0) }; 73 | 74 | if result == 0 { 75 | Err(io::Error::last_os_error()) 76 | } else { 77 | Ok(()) 78 | } 79 | } 80 | } 81 | 82 | impl Drop for NamedMutexGuard<'_> { 83 | fn drop(&mut self) { 84 | if !self.0.is_null() { 85 | let result = unsafe { self.unlock_core() }; 86 | debug_assert!( 87 | result.is_ok(), 88 | "Failed to unlock mutex: {:?}", 89 | result.unwrap_err() 90 | ); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /burnt-sushi/src/logger/file.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use std::{ 4 | fs::{self, File}, 5 | io::{BufWriter, Read, Seek, SeekFrom, Write}, 6 | path::PathBuf, 7 | time::Instant, 8 | }; 9 | 10 | use anyhow::Context; 11 | 12 | use super::SimpleLog; 13 | 14 | #[derive(Debug)] 15 | pub struct FileLog { 16 | path: PathBuf, 17 | file: Option>, 18 | last_written: Option, 19 | } 20 | 21 | impl FileLog { 22 | pub fn new(path: impl Into) -> Self { 23 | Self { 24 | path: path.into(), 25 | file: None, 26 | last_written: None, 27 | } 28 | } 29 | 30 | fn open_file(&mut self) -> anyhow::Result<&mut BufWriter> { 31 | if let Some(ref mut file) = self.file { 32 | return Ok(file); 33 | } 34 | 35 | if let Some(dir) = self.path.parent() { 36 | fs::create_dir_all(dir).context("Failed to create parent directories for log file.")?; 37 | } 38 | let mut file = File::options() 39 | .create(true) 40 | .append(true) 41 | .open(&self.path) 42 | .context("Failed to open or create log file.")?; 43 | 44 | if file.metadata().unwrap().len() > 10 * 1024 * 1024 45 | /* 10mb */ 46 | { 47 | file = File::options() 48 | .write(true) 49 | .read(true) 50 | .open(&self.path) 51 | .context("Failed to open or create log file.")?; 52 | let mut contents = String::new(); 53 | file.read_to_string(&mut contents) 54 | .context("Failed to read log file.")?; 55 | 56 | let mut truncated_contents = String::new(); 57 | for (index, _) in contents.match_indices('\n') { 58 | let succeeding = &contents[(index + 1)..]; 59 | if succeeding.len() > 1024 * 1024 60 | /* 1mb */ 61 | { 62 | continue; 63 | } 64 | truncated_contents.push_str(succeeding); 65 | break; 66 | } 67 | file.seek(SeekFrom::Start(0)) 68 | .context("Failed to seek in log file.")?; 69 | file.set_len(0).context("Failed to clear log file.")?; 70 | file.write_all(truncated_contents.as_bytes()) 71 | .context("Failed to write log file.")?; 72 | } 73 | let writer = BufWriter::new(file); 74 | Ok(self.file.insert(writer)) 75 | } 76 | } 77 | 78 | impl SimpleLog for FileLog { 79 | fn log(&mut self, message: &str) { 80 | let file = self 81 | .open_file() 82 | .context("Failed to prepare log file.") 83 | .unwrap(); 84 | writeln!(file, "{message}").unwrap(); 85 | file.flush().unwrap(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /burnt-sushi/src/args.rs: -------------------------------------------------------------------------------- 1 | use std::{path::PathBuf, sync::LazyLock}; 2 | 3 | use clap::{Parser, ValueEnum}; 4 | 5 | use crate::logger; 6 | 7 | pub static ARGS: LazyLock = LazyLock::new(|| { 8 | // Try to attach console for printing errors during argument parsing. 9 | logger::console::raw::attach(); 10 | 11 | Args::parse() 12 | }); 13 | 14 | #[derive(Parser, Debug)] 15 | #[command(author, version, about, long_about = None)] 16 | #[allow(clippy::struct_excessive_bools)] 17 | pub struct Args { 18 | /// Show a console window with debug output. 19 | #[arg(long)] 20 | pub console: bool, 21 | 22 | /// Do not attach to a parent console to show debug output. 23 | #[arg(long)] 24 | #[arg(conflicts_with("console"))] 25 | pub no_attach: bool, 26 | 27 | /// Level of debug output. 28 | #[arg(long, value_enum, default_value = "debug")] 29 | pub log_level: LogLevel, 30 | 31 | /// Path to a log file to write to. 32 | #[arg(long)] 33 | pub log_file: Option, 34 | 35 | /// Start a new instance of this app even if one is already running. 36 | #[arg(long)] 37 | pub ignore_singleton: bool, 38 | 39 | /// Exit program once spotify is closed, will wait for spotify to start if not currently running. 40 | #[arg(long)] 41 | pub shutdown_with_spotify: bool, 42 | 43 | /// Path to the blocker module. 44 | /// If the file doesn't exist it will be created with the default blocker. 45 | /// If not specified the app will try to find it in the same directory as the app with name `burnt-sushi-blocker-x86.dll` or write it to a temp file. 46 | #[arg(long)] 47 | pub blocker: Option, 48 | 49 | /// Path to the filter config. 50 | /// If the file doesn't exist it will be created with the default config. 51 | /// If not specified the app will try to find it in the same directory as the app named `filter.toml`. 52 | #[arg(long)] 53 | pub filters: Option, 54 | 55 | #[arg(long, hide = true)] 56 | pub install: bool, 57 | 58 | #[arg(long, hide = false)] 59 | pub update_old_bin: Option, 60 | 61 | #[arg(long, hide = true)] 62 | pub update_elevate_restart: bool, 63 | 64 | #[arg(long, hide = true)] 65 | pub singleton_wait_for_shutdown: bool, 66 | 67 | #[arg(long, hide = true)] 68 | pub autostart: bool, 69 | 70 | #[arg(long, hide = true)] 71 | pub force_restart: bool, 72 | } 73 | 74 | #[derive(ValueEnum, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] 75 | pub enum LogLevel { 76 | Off, 77 | Trace, 78 | Debug, 79 | Info, 80 | Warn, 81 | Error, 82 | } 83 | 84 | impl LogLevel { 85 | pub fn into_level_filter(self) -> log::LevelFilter { 86 | match self { 87 | LogLevel::Off => log::LevelFilter::Off, 88 | LogLevel::Trace => log::LevelFilter::Trace, 89 | LogLevel::Debug => log::LevelFilter::Debug, 90 | LogLevel::Info => log::LevelFilter::Info, 91 | LogLevel::Warn => log::LevelFilter::Warn, 92 | LogLevel::Error => log::LevelFilter::Error, 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /burnt-sushi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "burnt-sushi" 3 | version = "0.3.2" 4 | description = "Spotify AdBlocker for Windows" 5 | readme = "../README.md" 6 | repository = "https://github.com/OpenByteDev/burnt-sushi" 7 | license = "MIT" 8 | authors = ["OpenByte "] 9 | edition = "2024" 10 | keywords = ["spotify", "adblocker", "windows", "blocker"] 11 | 12 | [dependencies] 13 | dll-syringe = { version = "0.17.0", features = ["into-x86-from-x64", "rpc"], default-features = false } 14 | capnp = { version = "0.23.0", features = ["alloc"], default-features = false } 15 | capnp-rpc = { version = "0.23.0", default-features = false } 16 | toml = { version = "0.9.8", features = ["serde", "parse"], default-features = false } 17 | serde = { version = "1.0.204", features = ["derive"], default-features = false } 18 | futures = { version = "0.3.30", default-features = false } 19 | tokio = { version = "1.38.1", features = ["net", "rt", "macros", "fs", "sync"], default-features = false } 20 | tokio-util = { version = "0.7.11", features = ["compat"], default-features = false } 21 | winapi = { version = "0.3.9", features = ["winuser", "tlhelp32"], default-features = false } 22 | wineventhook = { version = "0.10.0", default-features = false } 23 | project-uninit = { version = "0.1.1", default-features = false } 24 | fallible-iterator = { version = "0.3.0", default-features = false } 25 | async-thread = { version = "0.1.2", default-features = false } 26 | log = { version = "0.4.22", default-features = false } 27 | shared = { path = "../shared", default-features = false } 28 | native-windows-gui = { version = "1.0.13", default-features = false, features = ["tray-notification", "message-window", "menu", "cursor", "image-decoder", "embed-resource"] } 29 | native-windows-derive = { version = "1.0.5", default-features = false } 30 | pipedconsole = { version = "0.3.2", default-features = false } 31 | widestring = { version = "1.1.0", default-features = false } 32 | ctrlc = { version = "3.4.4", default-features = false } 33 | clap = { version = "4.5.9", default-features = false, features = ["std", "derive", "help"] } 34 | is_elevated = { version = "0.1.2", default-features = false } 35 | self_update = { version = "0.42.0", default-features = false, features = ["rustls", "archive-zip"] } 36 | winrt-toast = { version = "0.1.1", default-features = false } 37 | faccess = { version = "0.2.4", default-features = false } 38 | semver = { version = "1.0.23", default-features = false } 39 | lenient_semver = { version = "0.4.2", default-features = false, features = ["semver"] } 40 | tempfile = { version = "3.10.1", default-features = false } 41 | reqwest = { version = "0.12.5", default-features = false } 42 | anyhow = { version = "1.0.86", default-features = false, features = ["std", "backtrace"] } 43 | dirs = { version = "6.0.0", default-features = false } 44 | chrono = { version = "0.4.38", default-features = false, features = ["std", "clock"] } 45 | 46 | [build-dependencies] 47 | cargo-emit = "0.2.1" 48 | winres = "0.1.12" 49 | 50 | [profile.release] 51 | strip = true 52 | lto = true 53 | opt-level = 3 54 | codegen-units = 1 55 | 56 | [[bin]] 57 | name = "BurntSushi" 58 | path = "src/main.rs" 59 | -------------------------------------------------------------------------------- /filter.toml: -------------------------------------------------------------------------------- 1 | # source: https://github.com/abba23/spotify-adblock/blob/main/config.toml 2 | 3 | allowlist = [ 4 | 'localhost', # local proxies 5 | 'audio-sp-.*\.pscdn\.co', # audio 6 | 'audio-fa\.scdn\.co', # audio 7 | 'audio4-fa\.scdn\.co', # audio 8 | 'charts-images\.scdn\.co', # charts images 9 | 'daily-mix\.scdn\.co', # daily mix images 10 | 'dailymix-images\.scdn\.co', # daily mix images 11 | 'heads-fa\.scdn\.co', # audio (heads) 12 | 'i\.scdn\.co', # cover art 13 | 'lineup-images\.scdn\.co', # playlists lineup images 14 | 'merch-img\.scdn\.co', # merch images 15 | 'misc\.scdn\.co', # miscellaneous images 16 | 'mosaic\.scdn\.co', # playlist mosaic images 17 | 'newjams-images\.scdn\.co', # release radar images 18 | 'o\.scdn\.co', # cover art 19 | 'pl\.scdn\.co', # playlist images 20 | 'profile-images\.scdn\.co', # artist profile images 21 | 'seeded-session-images\.scdn\.co', # radio images 22 | 't\.scdn\.co', # background images 23 | 'thisis-images\.scdn\.co', # 'this is' playlists images 24 | 'video-fa\.scdn\.co', # videos 25 | '.*\.acast\.com', # podcasts 26 | 'content\.production\.cdn\.art19\.com', # podcasts 27 | 'rss\.art19\.com', # podcasts 28 | '.*\.buzzsprout\.com', # podcasts 29 | 'chtbl\.com', # podcasts 30 | 'platform-lookaside\.fbsbx\.com', # Facebook profile images 31 | 'genius\.com', # lyrics (genius-spicetify) 32 | '.*\.googlevideo\.com', # YouTube videos (Spicetify Reddit app) 33 | '.*\.gvt1\.com', # Widevine download 34 | 'content\.libsyn\.com', # podcasts 35 | 'hwcdn\.libsyn\.com', # podcasts 36 | 'traffic\.libsyn\.com', # podcasts 37 | 'api.*-desktop\.musixmatch\.com', # lyrics (genius-spicetify) 38 | '.*\.podbean\.com', # podcasts 39 | 'cdn\.podigee\.com', # podcasts 40 | 'dts\.podtrac\.com', # podcasts 41 | 'www\.podtrac\.com', # podcasts 42 | 'www\.reddit\.com', # Reddit (Spicetify Reddit app) 43 | 'audio\.simplecast\.com', # podcasts 44 | 'media\.simplecast\.com', # podcasts 45 | 'ap\.spotify\.com', # audio (access point) 46 | '.*\.ap\.spotify\.com', # access points 47 | 'ap-.*\.spotify\.com', # access points 48 | 'api\.spotify\.com', # client APIs 49 | 'api-partner\.spotify\.com', # album/artist pages 50 | 'xpui\.app\.spotify\.com', # user interface 51 | 'apresolve\.spotify\.com', # access point resolving 52 | 'clienttoken\.spotify\.com', # login 53 | '.*dealer.*\.spotify\.com', # websocket connections 54 | 'image-upload.*\.spotify\.com', # image uploading 55 | 'login.*\.spotify\.com', # login 56 | '.*-spclient\.spotify\.com', # client APIs 57 | 'spclient\.wg\.spotify\.com', # client APIs, ads/tracking (blocked in blacklist) 58 | 'audio-fa\.spotifycdn\.com', # audio 59 | 'mixed-media-images\.spotifycdn\.com', # mix images 60 | 'seed-mix-image\.spotifycdn\.com', # mix images 61 | 'api\.spreaker\.com', # podcasts 62 | 'download\.ted\.com', # podcasts 63 | 'www\.youtube\.com', # YouTube (Spicetify Reddit app) 64 | 'i\.ytimg\.com', # YouTube images (Spicetify Reddit app) 65 | 'chrt\.fm', # podcasts 66 | 'dcs.*\.megaphone\.fm', # podcasts 67 | 'traffic\.megaphone\.fm', # podcasts 68 | 'pdst\.fm', # podcasts 69 | 'audio-ak-spotify-com\.akamaized\.net', # audio 70 | 'audio-akp-spotify-com\.akamaized\.net', # audio 71 | 'audio4-ak-spotify-com\.akamaized\.net', # audio 72 | 'heads4-ak-spotify-com\.akamaized\.net', # audio (heads) 73 | '.*\.cloudfront\.net', # podcasts 74 | 'audio4-ak\.spotify\.com\.edgesuite\.net', # audio 75 | 'scontent.*\.fbcdn\.net', # Facebook profile images 76 | 'audio-sp-.*\.spotifycdn\.net', # audio 77 | 'dovetail\.prxu\.org', # podcasts 78 | 'dovetail-cdn\.prxu\.org', # podcasts 79 | ] 80 | 81 | denylist = [ 82 | 'https://spclient\.wg\.spotify\.com/ads/.*', # ads 83 | 'https://spclient\.wg\.spotify\.com/ad-logic/.*', # ads 84 | 'https://spclient\.wg\.spotify\.com/gabo-receiver-service/.*', # tracking 85 | ] 86 | 87 | -------------------------------------------------------------------------------- /burnt-sushi/src/tray.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | sync::atomic::{AtomicBool, Ordering}, 3 | thread, 4 | }; 5 | 6 | use native_windows_derive as nwd; 7 | use native_windows_gui as nwg; 8 | 9 | use nwd::NwgUi; 10 | use nwg::NativeUi; 11 | use winapi::um::{ 12 | processthreadsapi::GetCurrentThreadId, 13 | winuser::{PostThreadMessageW, WM_QUIT}, 14 | }; 15 | 16 | use crate::{ 17 | APP_NAME, 18 | logger::{self, Console}, 19 | }; 20 | 21 | static INITIALIZED: AtomicBool = AtomicBool::new(false); 22 | 23 | pub struct SystemTrayManager { 24 | ui_thread: Option>, 25 | exit_recv: tokio::sync::watch::Receiver, 26 | thread_id: u32, 27 | } 28 | 29 | impl SystemTrayManager { 30 | pub async fn build_and_run() -> Result { 31 | if !INITIALIZED.swap(true, Ordering::SeqCst) { 32 | nwg::init()?; 33 | } 34 | 35 | let (start_tx, start_rx) = tokio::sync::oneshot::channel(); 36 | let (exit_tx, exit_rx) = tokio::sync::watch::channel(false); 37 | 38 | let ui_thread = thread::spawn(move || { 39 | let _tray_icon = match SystemTrayIcon::build_ui(SystemTrayIcon::default()) { 40 | Ok(tray_icon) => tray_icon, 41 | Err(err) => { 42 | start_tx.send(Err(err)).unwrap(); 43 | exit_tx.send(true).unwrap(); 44 | return; 45 | } 46 | }; 47 | 48 | let thread_id = unsafe { GetCurrentThreadId() }; 49 | start_tx.send(Ok(thread_id)).unwrap(); 50 | 51 | nwg::dispatch_thread_events(); 52 | 53 | exit_tx.send(true).unwrap(); 54 | }); 55 | 56 | Ok(Self { 57 | ui_thread: Some(ui_thread), 58 | thread_id: start_rx.await.unwrap()?, 59 | exit_recv: exit_rx, 60 | }) 61 | } 62 | 63 | pub async fn wait_for_exit(&mut self) { 64 | if self.ui_thread.is_none() || *self.exit_recv.borrow() { 65 | return; 66 | } 67 | 68 | self.exit_recv.changed().await.unwrap(); 69 | 70 | if let Some(ui_thread) = self.ui_thread.take() { 71 | ui_thread.join().unwrap(); 72 | } 73 | } 74 | 75 | pub async fn exit(mut self) { 76 | unsafe { PostThreadMessageW(self.thread_id, WM_QUIT, 0, 0) }; 77 | self.wait_for_exit().await; 78 | } 79 | } 80 | 81 | #[derive(NwgUi, Default)] 82 | pub struct SystemTrayIcon { 83 | #[nwg_control] 84 | window: nwg::MessageWindow, 85 | 86 | #[nwg_resource] 87 | embed: nwg::EmbedResource, 88 | 89 | #[nwg_resource(source_embed: Some(&data.embed), source_embed_str: Some("TRAYICON"))] 90 | icon: nwg::Icon, 91 | 92 | #[nwg_control(icon: Some(&data.icon), tip: Some(APP_NAME))] 93 | #[nwg_events(MousePressLeftUp: [SystemTrayIcon::show_menu], OnContextMenu: [SystemTrayIcon::show_menu])] 94 | tray: nwg::TrayNotification, 95 | 96 | #[nwg_control(parent: window, popup: true)] 97 | tray_menu: nwg::Menu, 98 | 99 | #[nwg_control(parent: tray_menu, text: "Show Console")] 100 | #[nwg_events(OnMenuItemSelected: [SystemTrayIcon::show_console])] 101 | tray_item2: nwg::MenuItem, 102 | 103 | #[nwg_control(parent: tray_menu, text: "Exit")] 104 | #[nwg_events(OnMenuItemSelected: [SystemTrayIcon::exit])] 105 | tray_item3: nwg::MenuItem, 106 | } 107 | 108 | #[allow(clippy::unused_self)] 109 | impl SystemTrayIcon { 110 | fn exit(&self) { 111 | nwg::stop_thread_dispatch(); 112 | } 113 | 114 | fn show_menu(&self) { 115 | let (x, y) = nwg::GlobalCursor::position(); 116 | 117 | let log = logger::global::get(); 118 | let has_console = log.console.is_some(); 119 | self.tray_item2.set_enabled(!has_console); 120 | self.tray_menu.popup(x, y); 121 | } 122 | 123 | fn show_console(&self) { 124 | let mut l = logger::global::get(); 125 | if l.console.is_none() { 126 | l.console = Some(Console::piped().unwrap()); 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /shared/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.4" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "capnp" 16 | version = "0.23.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "bbffb6e69e1ea2cf518ac5037a78a223ecb82b4ac81e7ab33cb5211b43d712ac" 19 | dependencies = [ 20 | "embedded-io", 21 | ] 22 | 23 | [[package]] 24 | name = "capnpc" 25 | version = "0.23.2" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "3f1cd4f042d6725da6245bde08c9e7c74f6fcb2d8bd5e0378e57991be8d711d8" 28 | dependencies = [ 29 | "capnp", 30 | ] 31 | 32 | [[package]] 33 | name = "cargo-emit" 34 | version = "0.2.1" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "1582e1c9e755dd6ad6b224dcffb135d199399a4568d454bd89fe515ca8425695" 37 | 38 | [[package]] 39 | name = "embedded-io" 40 | version = "0.7.1" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "9eb1aa714776b75c7e67e1da744b81a129b3ff919c8712b5e1b32252c1f07cc7" 43 | 44 | [[package]] 45 | name = "enum-map" 46 | version = "2.7.3" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" 49 | dependencies = [ 50 | "enum-map-derive", 51 | ] 52 | 53 | [[package]] 54 | name = "enum-map-derive" 55 | version = "0.17.0" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" 58 | dependencies = [ 59 | "proc-macro2", 60 | "quote", 61 | "syn", 62 | ] 63 | 64 | [[package]] 65 | name = "memchr" 66 | version = "2.7.6" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" 69 | 70 | [[package]] 71 | name = "proc-macro2" 72 | version = "1.0.103" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" 75 | dependencies = [ 76 | "unicode-ident", 77 | ] 78 | 79 | [[package]] 80 | name = "quote" 81 | version = "1.0.42" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" 84 | dependencies = [ 85 | "proc-macro2", 86 | ] 87 | 88 | [[package]] 89 | name = "regex" 90 | version = "1.12.2" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" 93 | dependencies = [ 94 | "aho-corasick", 95 | "memchr", 96 | "regex-automata", 97 | "regex-syntax", 98 | ] 99 | 100 | [[package]] 101 | name = "regex-automata" 102 | version = "0.4.13" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" 105 | dependencies = [ 106 | "aho-corasick", 107 | "memchr", 108 | "regex-syntax", 109 | ] 110 | 111 | [[package]] 112 | name = "regex-syntax" 113 | version = "0.8.8" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" 116 | 117 | [[package]] 118 | name = "shared" 119 | version = "0.3.2" 120 | dependencies = [ 121 | "capnp", 122 | "capnpc", 123 | "cargo-emit", 124 | "enum-map", 125 | "regex", 126 | ] 127 | 128 | [[package]] 129 | name = "syn" 130 | version = "2.0.111" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" 133 | dependencies = [ 134 | "proc-macro2", 135 | "quote", 136 | "unicode-ident", 137 | ] 138 | 139 | [[package]] 140 | name = "unicode-ident" 141 | version = "1.0.22" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" 144 | -------------------------------------------------------------------------------- /burnt-sushi/src/rpc.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use capnp::capability::Promise; 4 | use capnp_rpc::{RpcSystem, pry, rpc_twoparty_capnp, twoparty}; 5 | use futures::{AsyncReadExt, FutureExt}; 6 | use log::{debug, info}; 7 | use tokio::net::ToSocketAddrs; 8 | 9 | use crate::blocker::FilterConfig; 10 | 11 | struct LoggerImpl; 12 | 13 | impl shared::rpc::blocker_service::logger::Server for LoggerImpl { 14 | fn log_request( 15 | self: Rc, 16 | params: shared::rpc::blocker_service::logger::LogRequestParams, 17 | mut _results: shared::rpc::blocker_service::logger::LogRequestResults, 18 | ) -> impl futures::Future> + 'static { 19 | let request = pry!(pry!(params.get()).get_request()); 20 | 21 | let block_sign = if request.get_blocked() { '-' } else { '+' }; 22 | let hook_name = pry!(request.get_hook()); 23 | let url = pry!(request.get_url()); 24 | 25 | debug!( 26 | "[{}] ({}) {}", 27 | block_sign, 28 | hook_name, 29 | String::from_utf8_lossy(url.as_bytes()) 30 | ); 31 | 32 | Promise::ok(()) 33 | } 34 | 35 | fn log_message( 36 | self: Rc, 37 | params: shared::rpc::blocker_service::logger::LogMessageParams, 38 | mut _results: shared::rpc::blocker_service::logger::LogMessageResults, 39 | ) -> impl futures::Future> + 'static { 40 | let message = pry!(pry!(params.get()).get_message()); 41 | info!("{}", String::from_utf8_lossy(message.as_bytes())); 42 | 43 | Promise::ok(()) 44 | } 45 | } 46 | 47 | pub async fn run( 48 | socket_addr: impl ToSocketAddrs, 49 | filter_config: FilterConfig, 50 | ) -> Result<(), Box> { 51 | tokio::task::LocalSet::new() 52 | .run_until(async move { 53 | let stream = tokio::net::TcpStream::connect(socket_addr).await?; 54 | info!("Connected to {}", stream.peer_addr()?); 55 | 56 | stream.set_nodelay(true)?; 57 | let (reader, writer) = 58 | tokio_util::compat::TokioAsyncReadCompatExt::compat(stream).split(); 59 | let rpc_network = Box::new(twoparty::VatNetwork::new( 60 | reader, 61 | writer, 62 | rpc_twoparty_capnp::Side::Client, 63 | capnp::message::ReaderOptions::default(), 64 | )); 65 | let mut rpc_system = RpcSystem::new(rpc_network, None); 66 | let client: shared::rpc::blocker_service::Client = 67 | rpc_system.bootstrap(rpc_twoparty_capnp::Side::Server); 68 | 69 | let rpc = tokio::task::spawn_local(Box::pin(rpc_system.map(|_| ()))); 70 | 71 | let mut register_logger_request = client.register_logger_request(); 72 | register_logger_request 73 | .get() 74 | .set_logger(capnp_rpc::new_client(LoggerImpl)); 75 | register_logger_request.send().promise.await?; 76 | 77 | { 78 | let mut set_ruleset_request = client.set_ruleset_request(); 79 | set_ruleset_request 80 | .get() 81 | .set_hook(shared::rpc::blocker_service::FilterHook::GetAddrInfo); 82 | let mut ruleset = set_ruleset_request.get().init_ruleset(); 83 | let mut whitelist = ruleset 84 | .reborrow() 85 | .init_whitelist(filter_config.allowlist.len() as _); 86 | for (i, url) in filter_config.allowlist.iter().enumerate() { 87 | whitelist.set(i as _, url); 88 | } 89 | let mut _blacklist = ruleset.reborrow().init_blacklist(0); 90 | set_ruleset_request.send().promise.await?; 91 | } 92 | 93 | { 94 | let mut set_ruleset_request = client.set_ruleset_request(); 95 | set_ruleset_request 96 | .get() 97 | .set_hook(shared::rpc::blocker_service::FilterHook::CefUrlRequestCreate); 98 | let mut ruleset = set_ruleset_request.get().init_ruleset(); 99 | let mut blacklist = ruleset 100 | .reborrow() 101 | .init_blacklist(filter_config.denylist.len() as _); 102 | for (i, url) in filter_config.denylist.iter().enumerate() { 103 | blacklist.set(i as _, url); 104 | } 105 | let mut _whitelist = ruleset.reborrow().init_whitelist(0); 106 | set_ruleset_request.send().promise.await?; 107 | } 108 | 109 | let enable_filtering_request = client.enable_filtering_request(); 110 | enable_filtering_request.send().promise.await?; 111 | 112 | rpc.await.map_err(|e| e.into()) 113 | }) 114 | .await 115 | } 116 | -------------------------------------------------------------------------------- /burnt-sushi/src/resolver.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env, io, 3 | path::{Path, PathBuf}, 4 | }; 5 | 6 | use log::{debug, error, warn}; 7 | 8 | use crate::{ 9 | APP_AUTHOR, APP_NAME_WITH_VERSION, DEFAULT_BLOCKER_FILE_NAME, DEFAULT_FILTER_FILE_NAME, 10 | blocker::FilterConfig, 11 | }; 12 | 13 | pub async fn resolve_blocker(provided_path: Option<&Path>) -> io::Result { 14 | async fn try_load_blocker( 15 | path: &Path, 16 | check_len: bool, 17 | write_if_absent: bool, 18 | ) -> io::Result<()> { 19 | let payload_bytes = include_bytes!(concat!(env!("OUT_DIR"), "\\BurntSushiBlocker_x64.dll")); 20 | 21 | debug!("Looking for blocker at '{}'", path.display()); 22 | if let Ok(metadata) = tokio::fs::metadata(path).await { 23 | if metadata.is_file() { 24 | debug!("Found blocker at '{}'", path.display()); 25 | if check_len && metadata.len() != payload_bytes.len() as u64 { 26 | debug!( 27 | "Blocker at '{}' was ignored due to incorrect size.", 28 | path.display() 29 | ); 30 | } else { 31 | return Ok(()); 32 | } 33 | } 34 | } 35 | if write_if_absent { 36 | debug!("Writing blocker to '{}'", path.display()); 37 | tokio::fs::create_dir_all(path.parent().unwrap()).await?; 38 | tokio::fs::write(&path, payload_bytes).await?; 39 | Ok(()) 40 | } else { 41 | Err(io::Error::new( 42 | io::ErrorKind::NotFound, 43 | "Blocker not found at given path.", 44 | )) 45 | } 46 | } 47 | 48 | debug!("Looking for blocker according to cli args..."); 49 | if let Some(config_path) = provided_path { 50 | if try_load_blocker(config_path, false, true).await.is_ok() { 51 | return Ok(config_path.to_path_buf()); 52 | } else { 53 | debug!("Looking for blocker according to cli args..."); 54 | } 55 | } 56 | 57 | debug!("Looking for blocker next to executable..."); 58 | if let Some(sibling_path) = env::current_exe() 59 | .ok() 60 | .and_then(|p| p.parent().map(|p| p.join(DEFAULT_BLOCKER_FILE_NAME))) 61 | { 62 | if try_load_blocker(&sibling_path, false, false).await.is_ok() { 63 | return Ok(sibling_path); 64 | } 65 | } 66 | 67 | debug!("Looking for existing blocker in temporary directory..."); 68 | if let Some(temp_path) = env::temp_dir().parent().map(|p| { 69 | p.join(APP_AUTHOR) 70 | .join(APP_NAME_WITH_VERSION) 71 | .join(DEFAULT_BLOCKER_FILE_NAME) 72 | }) { 73 | if try_load_blocker(&temp_path, true, true).await.is_ok() { 74 | return Ok(temp_path); 75 | } 76 | } 77 | 78 | error!("Could not find or create blocker."); 79 | Err(io::Error::new( 80 | io::ErrorKind::NotFound, 81 | "Could not find or create blocker.", 82 | )) 83 | } 84 | 85 | pub async fn resolve_filter_config(provided_path: Option<&Path>) -> io::Result { 86 | async fn try_load_filter_config_from_path( 87 | path: Option<&Path>, 88 | write_if_absent: bool, 89 | ) -> io::Result { 90 | let default_filter_bytes = include_str!(concat!(env!("OUT_DIR"), "\\filter.toml")); 91 | 92 | if let Some(path) = path { 93 | debug!("Looking for filter config at '{}'", path.display()); 94 | if let Ok(filters) = tokio::fs::read_to_string(path).await { 95 | debug!("Found filter config at '{}'", path.display()); 96 | try_load_filter_config_from_str(&filters) 97 | } else if write_if_absent { 98 | debug!("Writing default filter config to '{}'", path.display()); 99 | tokio::fs::create_dir_all(path.parent().unwrap()).await?; 100 | tokio::fs::write(&path, default_filter_bytes).await?; 101 | try_load_filter_config_from_str(default_filter_bytes) 102 | } else { 103 | Err(io::Error::new( 104 | io::ErrorKind::NotFound, 105 | "Filter config did not exist.", 106 | )) 107 | } 108 | } else { 109 | debug!("Loading default filter config..."); 110 | try_load_filter_config_from_str(default_filter_bytes) 111 | } 112 | } 113 | 114 | fn try_load_filter_config_from_str(filter_config: &str) -> io::Result { 115 | if let Ok(filter_config) = toml::from_str(filter_config) { 116 | Ok(filter_config) 117 | } else { 118 | warn!("Failed to parse filter config."); 119 | Err(io::Error::new( 120 | io::ErrorKind::InvalidData, 121 | "Filter config is invalid.", 122 | )) 123 | } 124 | } 125 | 126 | debug!("Looking for filter config according to cli args..."); 127 | if let Some(config_path) = provided_path { 128 | if let Ok(filters) = try_load_filter_config_from_path(Some(config_path), true).await { 129 | return Ok(filters); 130 | } 131 | } 132 | 133 | debug!("Looking for filter config next to executable..."); 134 | if let Some(sibling_path) = env::current_exe() 135 | .ok() 136 | .and_then(|p| p.parent().map(|p| p.join(DEFAULT_FILTER_FILE_NAME))) 137 | { 138 | if let Ok(filters) = try_load_filter_config_from_path(Some(&sibling_path), false).await { 139 | return Ok(filters); 140 | } 141 | } 142 | 143 | try_load_filter_config_from_path(None, false).await 144 | } 145 | -------------------------------------------------------------------------------- /burnt-sushi/src/logger/console/log.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use std::{ 4 | fmt::Display, 5 | fs::File, 6 | io::{self, Write}, 7 | mem::{self, MaybeUninit}, 8 | os::windows::prelude::{AsRawHandle, FromRawHandle, IntoRawHandle, OwnedHandle}, 9 | ptr, 10 | }; 11 | 12 | use dll_syringe::process::{OwnedProcess, Process}; 13 | use project_uninit::partial_init; 14 | use widestring::U16CString; 15 | use winapi::{ 16 | shared::minwindef::{FALSE, TRUE}, 17 | um::{ 18 | handleapi::{CloseHandle, SetHandleInformation}, 19 | minwinbase::SECURITY_ATTRIBUTES, 20 | namedpipeapi::CreatePipe, 21 | processthreadsapi::{CreateProcessW, STARTUPINFOW}, 22 | winbase::{CREATE_NEW_CONSOLE, HANDLE_FLAG_INHERIT, STARTF_USESTDHANDLES}, 23 | }, 24 | }; 25 | 26 | use crate::{APP_NAME_WITH_VERSION, logger::SimpleLog}; 27 | 28 | use super::raw; 29 | 30 | #[derive(Debug)] 31 | pub struct Console(ConsoleImpl); 32 | 33 | #[derive(Debug)] 34 | enum ConsoleImpl { 35 | Attach, 36 | Alloc, 37 | Piped { process: OwnedProcess, pipe: File }, 38 | } 39 | 40 | unsafe impl Send for Console {} 41 | 42 | impl Console { 43 | pub fn attach() -> Option { 44 | raw::attach().then(|| Self(ConsoleImpl::Attach)) 45 | } 46 | pub fn alloc() -> Option { 47 | raw::alloc().then(|| Self(ConsoleImpl::Alloc)) 48 | } 49 | pub fn piped() -> io::Result { 50 | let mut security_attributes = SECURITY_ATTRIBUTES { 51 | nLength: mem::size_of::() as _, 52 | lpSecurityDescriptor: ptr::null_mut(), 53 | bInheritHandle: TRUE, // Set the bInheritHandle flag so pipe handles are inherited. 54 | }; 55 | let mut child_stdin_read_pipe = MaybeUninit::uninit(); 56 | let mut child_stdin_write_pipe = MaybeUninit::uninit(); 57 | // Create a pipe for the child process's STDIN. 58 | if unsafe { 59 | CreatePipe( 60 | child_stdin_read_pipe.as_mut_ptr(), 61 | child_stdin_write_pipe.as_mut_ptr(), 62 | &raw mut security_attributes, 63 | 0, 64 | ) 65 | } == FALSE 66 | { 67 | return Err(io::Error::last_os_error()); 68 | } 69 | let child_stdin_read_pipe = 70 | unsafe { OwnedHandle::from_raw_handle(child_stdin_read_pipe.assume_init()) }; 71 | let child_stdin_write_pipe = 72 | unsafe { OwnedHandle::from_raw_handle(child_stdin_write_pipe.assume_init()) }; 73 | 74 | // Ensure the write handle to the pipe for STDIN is not inherited. 75 | if unsafe { 76 | SetHandleInformation( 77 | child_stdin_write_pipe.as_raw_handle(), 78 | HANDLE_FLAG_INHERIT, 79 | 0, 80 | ) 81 | } == FALSE 82 | { 83 | return Err(io::Error::last_os_error()); 84 | } 85 | 86 | let mut title = U16CString::from_str(APP_NAME_WITH_VERSION).unwrap(); 87 | let mut command_line = 88 | U16CString::from_str("powershell.exe -Command \"for(;;) { $m = Read-Host }\"").unwrap(); 89 | let mut startup_info = MaybeUninit::::uninit(); 90 | partial_init!(startup_info => { 91 | cb: mem::size_of::() as _, 92 | lpReserved: ptr::null_mut(), 93 | lpDesktop: ptr::null_mut(), 94 | lpTitle: title.as_mut_ptr(), 95 | cbReserved2: 0, 96 | dwFlags: STARTF_USESTDHANDLES, 97 | lpReserved2: ptr::null_mut(), 98 | hStdInput: child_stdin_read_pipe.into_raw_handle(), 99 | hStdOutput: ptr::null_mut(), 100 | hStdError: ptr::null_mut(), 101 | }); 102 | 103 | let mut process_info = MaybeUninit::uninit(); 104 | let result = unsafe { 105 | CreateProcessW( 106 | ptr::null_mut(), 107 | command_line.as_mut_ptr(), 108 | ptr::null_mut(), 109 | ptr::null_mut(), 110 | TRUE, 111 | CREATE_NEW_CONSOLE, 112 | ptr::null_mut(), 113 | ptr::null_mut(), 114 | startup_info.as_mut_ptr(), 115 | process_info.as_mut_ptr(), 116 | ) 117 | }; 118 | if result == FALSE { 119 | return Err(io::Error::last_os_error()); 120 | } 121 | 122 | let process_info = unsafe { process_info.assume_init() }; 123 | unsafe { CloseHandle(process_info.hThread) }; 124 | 125 | let process = unsafe { OwnedProcess::from_raw_handle(process_info.hProcess) }; 126 | let pipe = File::from(child_stdin_write_pipe); 127 | Ok(Self(ConsoleImpl::Piped { process, pipe })) 128 | } 129 | 130 | pub fn is_active(&self) -> bool { 131 | match &self.0 { 132 | ConsoleImpl::Attach | ConsoleImpl::Alloc => true, 133 | ConsoleImpl::Piped { process, .. } => process.is_alive(), 134 | } 135 | } 136 | pub fn is_attached(&self) -> bool { 137 | matches!(self.0, ConsoleImpl::Attach | ConsoleImpl::Alloc) 138 | } 139 | pub fn is_piped(&self) -> bool { 140 | matches!(self.0, ConsoleImpl::Piped { .. }) 141 | } 142 | 143 | pub fn println(&mut self, message: impl Display) -> io::Result<()> { 144 | match &mut self.0 { 145 | ConsoleImpl::Attach | ConsoleImpl::Alloc => println!("{message}"), 146 | ConsoleImpl::Piped { pipe, .. } => writeln!(pipe, "{message}")?, 147 | } 148 | Ok(()) 149 | } 150 | } 151 | 152 | impl SimpleLog for Console { 153 | fn log(&mut self, message: &str) { 154 | self.println(message).unwrap(); 155 | } 156 | } 157 | 158 | impl Drop for Console { 159 | fn drop(&mut self) { 160 | match self.0 { 161 | ConsoleImpl::Attach => {} 162 | ConsoleImpl::Alloc => raw::free(), 163 | ConsoleImpl::Piped { ref process, .. } => { 164 | let _ = process.kill(); 165 | } 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /burnt-sushi-blocker/src/hooks.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi::CStr, panic::AssertUnwindSafe, ptr, slice, sync::OnceLock}; 2 | 3 | use cef::{_cef_request_context_t, _cef_request_t, _cef_urlrequest_client_t, cef_urlrequest_t}; 4 | use dll_syringe::process::OwnedProcessModule; 5 | use retour::static_detour; 6 | use shared::rpc::blocker_service::FilterHook; 7 | use winapi::{ 8 | shared::{minwindef::INT, ntdef::PCSTR, ws2def::ADDRINFOA}, 9 | um::winsock2::WSAHOST_NOT_FOUND, 10 | }; 11 | 12 | use crate::{cef, filters::Filters, utils::panic_info_to_string}; 13 | 14 | type GetAddrInfoFn = 15 | unsafe extern "system" fn(PCSTR, PCSTR, *const ADDRINFOA, *const *const ADDRINFOA) -> INT; 16 | static_detour! { 17 | static GetAddrInfoHook: unsafe extern "system" fn(PCSTR, PCSTR, *const ADDRINFOA, *const *const ADDRINFOA) -> INT; 18 | } 19 | type CefUrlRequestCreateFn = unsafe extern "C" fn( 20 | *mut cef::_cef_request_t, 21 | *mut cef::_cef_urlrequest_client_t, 22 | *mut cef::_cef_request_context_t, 23 | ) -> *mut cef::cef_urlrequest_t; 24 | type CefStringUserfreeUtf16FreeFn = unsafe extern "C" fn(cef::cef_string_userfree_utf16_t); 25 | static_detour! { 26 | static CefUrlRequestCreateHook: unsafe extern "C" fn(*mut _cef_request_t, *mut _cef_urlrequest_client_t, *mut _cef_request_context_t) -> *mut cef_urlrequest_t; 27 | } 28 | 29 | pub enum LogParams { 30 | Message(String), 31 | Request { 32 | url: String, 33 | blocked: bool, 34 | hook: shared::rpc::blocker_service::FilterHook, 35 | }, 36 | } 37 | 38 | pub fn enable( 39 | filters: Filters, 40 | log_tx: tokio::sync::mpsc::UnboundedSender, 41 | ) -> Result<(), Box> { 42 | static GET_ADDR_INFO_HOOK: OnceLock<()> = OnceLock::new(); 43 | static CEF_URL_REQUEST_CREATE_HOOK: OnceLock<()> = OnceLock::new(); 44 | 45 | GET_ADDR_INFO_HOOK 46 | .get_or_try_init(|| init_get_addr_info_hook(filters.clone(), log_tx.clone()))?; 47 | CEF_URL_REQUEST_CREATE_HOOK 48 | .get_or_try_init(|| init_cef_urlrequest_create_hook(filters, log_tx))?; 49 | 50 | unsafe { GetAddrInfoHook.enable() }?; 51 | unsafe { CefUrlRequestCreateHook.enable() }?; 52 | 53 | Ok(()) 54 | } 55 | 56 | pub fn disable() -> Result<(), Box> { 57 | if GetAddrInfoHook.is_enabled() { 58 | unsafe { GetAddrInfoHook.disable() }?; 59 | } 60 | if CefUrlRequestCreateHook.is_enabled() { 61 | unsafe { CefUrlRequestCreateHook.disable()? }; 62 | } 63 | Ok(()) 64 | } 65 | 66 | fn init_get_addr_info_hook( 67 | filters: Filters, 68 | log_tx: tokio::sync::mpsc::UnboundedSender, 69 | ) -> Result<(), Box> { 70 | let ws2 = 71 | OwnedProcessModule::find_local_by_name("WS2_32.dll")?.ok_or("WS2_32.dll not found")?; 72 | let getaddrinfo = unsafe { ws2.get_local_procedure::("getaddrinfo")? }; 73 | unsafe { 74 | GetAddrInfoHook.initialize( 75 | getaddrinfo, 76 | move |node_name, service_name, hints, result| { 77 | let res = std::panic::catch_unwind(AssertUnwindSafe(|| { 78 | let url = CStr::from_ptr(node_name).to_str().unwrap(); // TODO: 79 | let block = !filters.check(FilterHook::CefUrlRequestCreate, url); 80 | 81 | let _ = log_tx.send(LogParams::Request { 82 | hook: shared::rpc::blocker_service::FilterHook::GetAddrInfo, 83 | blocked: block, 84 | url: url.to_string(), 85 | }); 86 | 87 | block 88 | })); 89 | 90 | let block = match res { 91 | Ok(block) => block, 92 | Err(e) => { 93 | let _ = log_tx.send(LogParams::Message(panic_info_to_string(e))); 94 | false 95 | } 96 | }; 97 | 98 | if block { 99 | WSAHOST_NOT_FOUND as _ 100 | } else { 101 | GetAddrInfoHook.call(node_name, service_name, hints, result) 102 | } 103 | }, 104 | ) 105 | }?; 106 | 107 | Ok(()) 108 | } 109 | 110 | fn init_cef_urlrequest_create_hook( 111 | filters: Filters, 112 | log_tx: tokio::sync::mpsc::UnboundedSender, 113 | ) -> Result<(), Box> { 114 | let libcef = 115 | OwnedProcessModule::find_local_by_name("libcef.dll")?.ok_or("libcef.dll not found")?; 116 | let cef_urlrequest_create = 117 | unsafe { libcef.get_local_procedure::("cef_urlrequest_create")? }; 118 | let cef_string_userfree_utf16_free = unsafe { 119 | libcef 120 | .get_local_procedure::("cef_string_userfree_utf16_free")? 121 | }; 122 | 123 | unsafe { 124 | CefUrlRequestCreateHook.initialize( 125 | cef_urlrequest_create, 126 | move |request, client, request_context| -> *mut cef::cef_urlrequest_t { 127 | let res = std::panic::catch_unwind(AssertUnwindSafe(|| { 128 | if request.is_null() { 129 | return false; 130 | } 131 | 132 | let cef_url = ((*request).get_url)(request); 133 | if cef_url.is_null() { 134 | return false; 135 | } 136 | 137 | let wide_url = slice::from_raw_parts((*cef_url).str_, (*cef_url).length as _); 138 | let url = String::from_utf16_lossy(wide_url); 139 | cef_string_userfree_utf16_free(cef_url); 140 | 141 | let block = !filters.check(FilterHook::CefUrlRequestCreate, &url); 142 | 143 | let _ = log_tx.send(LogParams::Request { 144 | hook: FilterHook::CefUrlRequestCreate, 145 | blocked: block, 146 | url, 147 | }); 148 | 149 | block 150 | })); 151 | 152 | let block = match res { 153 | Ok(block) => block, 154 | Err(e) => { 155 | let _ = log_tx.send(LogParams::Message(panic_info_to_string(e))); 156 | false 157 | } 158 | }; 159 | 160 | if block { 161 | ptr::null_mut() 162 | } else { 163 | CefUrlRequestCreateHook.call(request, client, request_context) 164 | } 165 | }, 166 | ) 167 | }?; 168 | 169 | Ok(()) 170 | } 171 | -------------------------------------------------------------------------------- /burnt-sushi/src/blocker.rs: -------------------------------------------------------------------------------- 1 | use std::{mem, net::SocketAddrV4}; 2 | 3 | use anyhow::Context; 4 | use dll_syringe::{ 5 | Syringe, 6 | error::SyringeError, 7 | process::{OwnedProcessModule, Process}, 8 | }; 9 | use log::{debug, error, info, warn}; 10 | use serde::Deserialize; 11 | use tokio::{runtime, task::LocalSet}; 12 | 13 | use crate::{ 14 | DEFAULT_BLOCKER_FILE_NAME, 15 | args::ARGS, 16 | resolver::{resolve_blocker, resolve_filter_config}, 17 | rpc, 18 | spotify_process_scanner::{SpotifyInfo, SpotifyProcessScanner, SpotifyState}, 19 | }; 20 | 21 | pub struct SpotifyAdBlocker { 22 | scanner: SpotifyProcessScanner, 23 | spotify_state: tokio::sync::watch::Receiver, 24 | state: SpotifyHookState, 25 | } 26 | 27 | #[allow(clippy::large_enum_variant)] 28 | enum SpotifyHookState { 29 | Hooked(HookState), 30 | Unhooked, 31 | } 32 | 33 | struct HookState { 34 | syringe: Syringe, 35 | payload: OwnedProcessModule, 36 | rpc_task: async_thread::JoinHandle<()>, 37 | } 38 | 39 | impl SpotifyAdBlocker { 40 | pub fn new() -> Self { 41 | let (scanner, spotify_state) = SpotifyProcessScanner::new(); 42 | Self { 43 | scanner, 44 | spotify_state, 45 | state: SpotifyHookState::Unhooked, 46 | } 47 | } 48 | 49 | pub async fn run(&mut self) { 50 | tokio::select! { 51 | _ = self.scanner.run() => { 52 | unreachable!("Spotify scanner should never stop on its own"); 53 | } 54 | _ = async { 55 | info!("Looking for Spotify..."); 56 | while self.spotify_state.changed().await.is_ok() { 57 | let state = self.spotify_state.borrow(); 58 | match &*state { 59 | SpotifyState::Running(spotify) => { 60 | self.state.hook_spotify(spotify.try_clone().unwrap()).await.unwrap(); 61 | }, 62 | SpotifyState::Stopped => { 63 | self.state.unhook_spotify().await; 64 | if ARGS.shutdown_with_spotify { 65 | info!("Shutting down due to spotify exit..."); 66 | break; 67 | } 68 | info!("Looking for Spotify..."); 69 | } 70 | } 71 | } 72 | } => {} 73 | } 74 | } 75 | 76 | pub async fn stop(&mut self) { 77 | if matches!(self.state, SpotifyHookState::Hooked(_)) { 78 | self.state.unhook_spotify().await; 79 | } 80 | } 81 | } 82 | 83 | impl SpotifyHookState { 84 | async fn hook_spotify(&mut self, spotify: SpotifyInfo) -> anyhow::Result<()> { 85 | if let SpotifyHookState::Hooked(_) = self { 86 | self.unhook_spotify().await; 87 | } 88 | 89 | match spotify.process.pid().ok() { 90 | Some(pid) => info!("Found Spotify (PID={pid})"), 91 | None => info!("Found Spotify"), 92 | } 93 | let syringe = Syringe::for_process(spotify.process); 94 | 95 | while let Some(prev_payload) = syringe 96 | .process() 97 | .find_module_by_name(DEFAULT_BLOCKER_FILE_NAME) 98 | .context("Failed to inspect modules of Spotify process.")? 99 | { 100 | warn!("Found previously injected blocker"); 101 | 102 | debug!("Stopping RPC of previous blocker"); 103 | let stop_rpc = 104 | unsafe { syringe.get_payload_procedure::(prev_payload, "stop_rpc") } 105 | .context("Failed to access spotify process.")? 106 | .context("Failed to find stop_rpc in blocker module.")?; 107 | match stop_rpc.call() { 108 | Ok(_) => { 109 | debug!("Stopped RPC of previous blocker"); 110 | } 111 | Err(e) => { 112 | error!("Failed to stop RPC of previous blocker: {e}"); 113 | } 114 | } 115 | 116 | info!("Ejecting previous blocker..."); 117 | match syringe.eject(prev_payload) { 118 | Ok(_) => info!("Ejected previous blocker"), 119 | Err(_) => error!("Failed to eject previous blocker"), 120 | } 121 | } 122 | 123 | info!("Loading filter config..."); 124 | let filter_config = resolve_filter_config(ARGS.filters.as_ref().map(|p| p.as_ref())) 125 | .await 126 | .context("Failed to resolve filter config.")?; 127 | 128 | info!("Preparing blocker..."); 129 | let payload_path = resolve_blocker(ARGS.blocker.as_ref().map(|p| p.as_ref())) 130 | .await 131 | .context("Failed to resolve blocker.")?; 132 | 133 | info!("Injecting blocker..."); 134 | let payload = syringe 135 | .inject(payload_path) 136 | .context("Failed to inject blocker.")?; 137 | 138 | debug!("Starting RPC..."); 139 | let start_rpc = 140 | unsafe { syringe.get_payload_procedure:: SocketAddrV4>(payload, "start_rpc") } 141 | .context("Failed to access spotify process.")? 142 | .context("Failed to find start_rpc in blocker module.")?; 143 | 144 | let rpc_socket_addr = start_rpc.call().unwrap(); 145 | 146 | let rpc_task = async_thread::spawn(move || { 147 | let rt = runtime::Builder::new_current_thread() 148 | .enable_all() 149 | .build() 150 | .unwrap(); 151 | let localset = LocalSet::new(); 152 | localset.block_on(&rt, async move { 153 | rpc::run(rpc_socket_addr, filter_config).await.unwrap(); 154 | }); 155 | }); 156 | 157 | info!("Blocker up and running!"); 158 | *self = SpotifyHookState::Hooked(HookState { 159 | payload: payload.try_to_owned().unwrap(), 160 | syringe, 161 | rpc_task, 162 | }); 163 | 164 | Ok(()) 165 | } 166 | 167 | async fn unhook_spotify(&mut self) { 168 | let state = mem::replace(self, SpotifyHookState::Unhooked); 169 | let state = match state { 170 | SpotifyHookState::Hooked(state) => state, 171 | SpotifyHookState::Unhooked => return, 172 | }; 173 | 174 | info!("Unhooking Spotify..."); 175 | 176 | let result: Result<(), SyringeError> = async { 177 | let stop_rpc = unsafe { 178 | state 179 | .syringe 180 | .get_payload_procedure::(state.payload.borrowed(), "stop_rpc") 181 | }? 182 | .unwrap(); 183 | 184 | debug!("Stopping RPC..."); 185 | stop_rpc.call()?; 186 | state.rpc_task.join().await.unwrap(); 187 | debug!("Stopped RPC"); 188 | 189 | if state.payload.process().is_alive() { 190 | info!("Ejecting blocker..."); 191 | state.syringe.eject(state.payload.borrowed())?; 192 | info!("Ejected blocker"); 193 | } 194 | 195 | Ok(()) 196 | } 197 | .await; 198 | 199 | match result { 200 | Ok(_) 201 | | Err(SyringeError::ProcessInaccessible | SyringeError::ModuleInaccessible) => {} 202 | _ => todo!("{:#?}", result), 203 | } 204 | 205 | *self = SpotifyHookState::Unhooked; 206 | } 207 | } 208 | 209 | #[derive(Deserialize, Debug)] 210 | pub struct FilterConfig { 211 | pub allowlist: Vec, 212 | pub denylist: Vec, 213 | } 214 | -------------------------------------------------------------------------------- /burnt-sushi/src/main.rs: -------------------------------------------------------------------------------- 1 | #![feature(once_cell_try, iter_intersperse, lazy_get)] 2 | #![warn(unsafe_op_in_unsafe_fn, clippy::pedantic)] 3 | #![allow( 4 | non_snake_case, 5 | clippy::module_inception, 6 | clippy::ignored_unit_patterns, 7 | clippy::cast_possible_truncation, 8 | clippy::collapsible_if, 9 | clippy::redundant_else, 10 | clippy::needless_pass_by_value, 11 | clippy::cast_possible_wrap, 12 | clippy::cast_sign_loss, 13 | clippy::redundant_closure_for_method_calls 14 | )] 15 | #![windows_subsystem = "windows"] 16 | 17 | use anyhow::{Context, anyhow}; 18 | use dll_syringe::process::{OwnedProcess, Process}; 19 | use log::{debug, error, info, trace, warn}; 20 | use winapi::{ 21 | shared::minwindef::FALSE, 22 | um::{processthreadsapi::OpenProcess, synchapi::WaitForSingleObject, winnt::PROCESS_TERMINATE}, 23 | }; 24 | 25 | use std::{ 26 | env, io, num::NonZero, os::windows::prelude::FromRawHandle, sync::LazyLock, time::Duration, 27 | }; 28 | 29 | use crate::{ 30 | args::{ARGS, LogLevel}, 31 | blocker::SpotifyAdBlocker, 32 | logger::{Console, FileLog}, 33 | named_mutex::NamedMutex, 34 | }; 35 | 36 | mod args; 37 | mod blocker; 38 | mod logger; 39 | mod named_mutex; 40 | mod resolver; 41 | mod rpc; 42 | mod spotify_process_scanner; 43 | mod tray; 44 | mod update; 45 | 46 | const APP_NAME: &str = "BurntSushi"; 47 | const APP_AUTHOR: &str = "OpenByteDev"; 48 | const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); 49 | const APP_NAME_WITH_VERSION: &str = concat!("BurntSushi v", env!("CARGO_PKG_VERSION")); 50 | const DEFAULT_BLOCKER_FILE_NAME: &str = "BurntSushiBlocker_x64.dll"; 51 | const DEFAULT_FILTER_FILE_NAME: &str = "filter.toml"; 52 | 53 | #[tokio::main(flavor = "current_thread")] 54 | async fn main() { 55 | logger::global::init(); 56 | 57 | log::set_max_level(ARGS.log_level.into_level_filter()); 58 | 59 | if !ARGS.no_attach { 60 | if let Some(console) = Console::attach() { 61 | logger::global::get().console = Some(console); 62 | debug!("Attached to console"); 63 | } 64 | } 65 | 66 | if ARGS.console { 67 | if let Some(console) = Console::alloc() { 68 | logger::global::get().console = Some(console); 69 | debug!("Allocated new console"); 70 | } 71 | } 72 | 73 | let mut log_file = ARGS.log_file.clone(); 74 | if log_file.is_none() && ARGS.log_level == LogLevel::Debug { 75 | let mut auto_log_file = dirs::data_dir(); 76 | if let Some(ref mut log_file) = auto_log_file { 77 | log_file.push("OpenByte"); 78 | log_file.push("BurntSushi"); 79 | log_file.push("BurntSushi.log"); 80 | } 81 | log_file = auto_log_file; 82 | } 83 | if let Some(log_file) = log_file { 84 | logger::global::get().file = Some(FileLog::new(log_file)); 85 | } 86 | 87 | info!("{APP_NAME_WITH_VERSION}"); 88 | trace!( 89 | "Running from {}", 90 | env::current_exe() 91 | .unwrap_or_else(|_| "".into()) 92 | .display() 93 | ); 94 | trace!("Running with {:#?}", LazyLock::get(&ARGS).unwrap()); 95 | 96 | if ARGS.install { 97 | match handle_install().await { 98 | Ok(()) => info!("App successfully installed."), 99 | Err(e) => error!("Failed to install application: {e}"), 100 | } 101 | return; 102 | } 103 | 104 | if let Some(old_bin_path) = &ARGS.update_old_bin { 105 | tokio::task::spawn(tokio::fs::remove_file(old_bin_path)); 106 | } 107 | 108 | if ARGS.force_restart { 109 | match terminate_other_instances() { 110 | Ok(_) => debug!("Killed previously running instances"), 111 | Err(err) => { 112 | error!("Failed to open previously running instance (err={err})"); 113 | return; 114 | } 115 | } 116 | } 117 | 118 | if ARGS.ignore_singleton { 119 | run().await; 120 | } else { 121 | let lock = NamedMutex::new(&format!("{APP_NAME} SINGLETON MUTEX")).unwrap(); 122 | 123 | let mut guard_result = lock.try_lock(); 124 | 125 | if ARGS.singleton_wait_for_shutdown { 126 | while matches!(guard_result, Ok(None)) { 127 | tokio::time::sleep(Duration::from_millis(100)).await; 128 | guard_result = lock.try_lock(); 129 | } 130 | } 131 | 132 | match guard_result { 133 | Ok(Some(_guard)) => run().await, 134 | Ok(None) => { 135 | error!("App is already running. (use --ignore-singleton to ignore)\nExiting..."); 136 | } 137 | Err(e) => { 138 | error!("Failed to lock singleton mutex: {e} (use --ignore-singleton to ignore) "); 139 | } 140 | } 141 | } 142 | 143 | logger::global::unset(); 144 | } 145 | 146 | async fn run() { 147 | let mut system_tray = tray::SystemTrayManager::build_and_run().await.unwrap(); 148 | 149 | let mut app = SpotifyAdBlocker::new(); 150 | 151 | let (update_restart_tx, update_restart_rx) = tokio::sync::oneshot::channel(); 152 | tokio::task::spawn(async move { 153 | match update::update().await { 154 | Ok(true) => update_restart_tx.send(()).unwrap(), 155 | Ok(false) => {} 156 | Err(e) => error!("App update failed: {e:#}"), 157 | } 158 | }); 159 | 160 | tokio::select! { 161 | _ = app.run() => { 162 | } 163 | _ = wait_for_ctrl_c() => { 164 | debug!("Ctrl-C received"); 165 | } 166 | _ = system_tray.wait_for_exit() => { 167 | debug!("System tray exited"); 168 | } 169 | Ok(_) = update_restart_rx => { 170 | debug!("Shutting down due to update"); 171 | } 172 | } 173 | 174 | info!("Shutting down..."); 175 | 176 | app.stop().await; 177 | system_tray.exit().await; 178 | 179 | info!("Exiting..."); 180 | } 181 | 182 | async fn wait_for_ctrl_c() -> Result<(), ctrlc::Error> { 183 | let (tx, rx) = tokio::sync::oneshot::channel(); 184 | let mut handler = Some(move || tx.send(()).unwrap()); 185 | ctrlc::set_handler(move || { 186 | if let Some(h) = handler.take() { 187 | h(); 188 | } 189 | })?; 190 | rx.await.unwrap(); 191 | Ok(()) 192 | } 193 | 194 | async fn handle_install() -> anyhow::Result<()> { 195 | if !is_elevated::is_elevated() { 196 | return Err(anyhow!("Must be run as administrator")); 197 | } 198 | 199 | let current_location = env::current_exe().context("Failed to locate current executable")?; 200 | let blocker_location = current_location 201 | .parent() 202 | .ok_or_else(|| anyhow!("Failed to determine parent directory"))? 203 | .join(DEFAULT_BLOCKER_FILE_NAME); 204 | resolver::resolve_blocker(Some(&blocker_location)) 205 | .await 206 | .context("Failed to write blocker to disk")?; 207 | 208 | Ok(()) 209 | } 210 | 211 | fn terminate_other_instances() -> anyhow::Result<()> { 212 | let other_processes = OwnedProcess::find_all_by_name(APP_NAME) 213 | .into_iter() 214 | .filter(|p| !p.is_current()); 215 | 216 | for process in other_processes { 217 | let handle = unsafe { 218 | OpenProcess( 219 | PROCESS_TERMINATE, 220 | FALSE, 221 | process.pid().map_or(0, NonZero::get), 222 | ) 223 | }; 224 | 225 | if handle.is_null() { 226 | Err(io::Error::last_os_error()).context("Failed to access other running instances")?; 227 | } 228 | 229 | let process = unsafe { OwnedProcess::from_raw_handle(handle) }; 230 | process.kill().context("Failed to kill process.")?; 231 | 232 | let _ = unsafe { WaitForSingleObject(handle, Duration::from_secs(5).as_millis() as _) }; 233 | } 234 | 235 | Ok(()) 236 | } 237 | -------------------------------------------------------------------------------- /burnt-sushi/src/update.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env, 3 | io::{self, Write}, 4 | path::Path, 5 | process::Stdio, 6 | ptr, 7 | }; 8 | 9 | use anyhow::Context; 10 | use log::{debug, error, info}; 11 | use reqwest::header::HeaderValue; 12 | use self_update::update::Release; 13 | use tokio::fs::{self, File}; 14 | use widestring::{U16CString, u16cstr}; 15 | use winapi::um::{shellapi::ShellExecuteW, winuser::SW_SHOWDEFAULT}; 16 | use winrt_toast::{Action, Text, Toast, ToastManager}; 17 | 18 | use crate::{APP_NAME, APP_VERSION, ARGS}; 19 | 20 | pub async fn update() -> anyhow::Result { 21 | let releases = tokio::task::spawn_blocking(load_releases) 22 | .await 23 | .context("Failed to load releases")? 24 | .context("Failed to load releases")?; 25 | 26 | let (release, release_version) = releases 27 | .into_iter() 28 | .filter_map(|r| lenient_semver::parse(&r.version).ok().map(|v| (r, v))) 29 | .max_by(|(_, v1), (_, v2)| v1.cmp(v2)) 30 | .context("No valid release found")?; 31 | 32 | if release_version <= lenient_semver::parse(APP_VERSION).unwrap() { 33 | info!("No new release found"); 34 | return Ok(false); 35 | } 36 | 37 | if !ARGS.update_elevate_restart { 38 | if confirm_update(&release.version).await { 39 | debug!("Update confirmed"); 40 | } else { 41 | debug!("Update ignored"); 42 | return Ok(false); 43 | } 44 | } 45 | 46 | let current_exe = env::current_exe() 47 | .and_then(|p| p.canonicalize()) 48 | .context("Failed to locate current executable")?; 49 | let needs_elevation = !faccess::PathExt::writable(current_exe.parent().unwrap()); 50 | if needs_elevation { 51 | debug!("Elevation is required for update"); 52 | if is_elevated::is_elevated() { 53 | debug!("Already running elevated"); 54 | } else { 55 | debug!("Not currently elevated"); 56 | debug!("Restarting app elevated"); 57 | 58 | restart_elevated().context("Failed to restart with elevation")?; 59 | 60 | return Ok(true); 61 | } 62 | } else { 63 | debug!("Elevation is not required for update"); 64 | } 65 | 66 | let asset = release 67 | .assets 68 | .into_iter() 69 | .find(|asset| { 70 | Path::new(&asset.name) 71 | .extension() 72 | .is_some_and(|ext| ext.eq_ignore_ascii_case("exe")) 73 | }) 74 | .context("No release executable asset found")?; 75 | 76 | debug!( 77 | "Found release asset [{}] at {}", 78 | asset.name, asset.download_url 79 | ); 80 | 81 | let tmp_dir = tempfile::Builder::new() 82 | .prefix(APP_NAME) 83 | .tempdir() 84 | .context("Failed to create temporary directory")?; 85 | let tmp_bin_path = tmp_dir.path().join(&asset.name); 86 | let tmp_bin = File::create(&tmp_bin_path) 87 | .await 88 | .context("Error creating temporary file")? 89 | .into_std() 90 | .await; 91 | 92 | tokio::task::spawn_blocking(move || download_file(&asset.download_url, tmp_bin)) 93 | .await 94 | .context("Error downloading updated executable")? 95 | .context("Error downloading updated executable")?; 96 | 97 | debug!("Downloaded asset to {}", tmp_bin_path.display()); 98 | 99 | let moved_bin = current_exe.with_extension("exe.bak"); 100 | 101 | fs::rename(¤t_exe, &moved_bin) 102 | .await 103 | .context("Failed to move current executable")?; 104 | match fs::rename(&tmp_bin_path, ¤t_exe).await { 105 | Ok(_) => {} 106 | Err(e) if e.raw_os_error() == Some(17) => { 107 | fs::copy(&tmp_bin_path, ¤t_exe) 108 | .await 109 | .context("Failed to copy updated executable to current executable path")?; 110 | } 111 | Err(e) => { 112 | return Err(e) 113 | .context("Failed to move updated executable to current executable path")?; 114 | } 115 | } 116 | 117 | debug!("Switched out binary"); 118 | 119 | restart(¤t_exe, &moved_bin).context("Failed to restart with updated executable")?; 120 | 121 | Ok(true) 122 | } 123 | 124 | fn restart(new_exe: &Path, old_exe: &Path) -> anyhow::Result<()> { 125 | let current_args = env::args().skip(1); 126 | 127 | std::process::Command::new(new_exe) 128 | .args(current_args) 129 | .arg("--update-old-bin") 130 | .arg(old_exe) 131 | .arg("--singleton-wait-for-shutdown") 132 | .stdin(Stdio::inherit()) 133 | .spawn() 134 | .context("Error spawning process")?; 135 | 136 | Ok(()) 137 | } 138 | 139 | fn restart_elevated() -> anyhow::Result<()> { 140 | let exe = U16CString::from_os_str( 141 | env::current_exe() 142 | .context("Failed to locate current executable")? 143 | .into_os_string(), 144 | ) 145 | .context("Current executable has an invalid path?")?; 146 | let current_args = env::args().skip(1).collect::>().join(" "); 147 | let new_args = "--update-elevate-restart --singleton-wait-for-shutdown"; 148 | let args = U16CString::from_str(format!("{current_args} {new_args}")) 149 | .context("Arguments contain invalid characters")?; 150 | 151 | let result = unsafe { 152 | ShellExecuteW( 153 | ptr::null_mut(), 154 | u16cstr!("runas").as_ptr(), 155 | exe.as_ptr(), 156 | args.as_ptr(), 157 | ptr::null_mut(), 158 | SW_SHOWDEFAULT, 159 | ) 160 | }; 161 | 162 | if result <= 32 as _ { 163 | return Err(io::Error::last_os_error()).context("Failed to run ShellExecuteW"); 164 | } 165 | 166 | Ok(()) 167 | } 168 | 169 | fn load_releases() -> Result, self_update::errors::Error> { 170 | self_update::backends::github::ReleaseList::configure() 171 | .repo_owner("OpenByteDev") 172 | .repo_name("burnt-sushi") 173 | .build()? 174 | .fetch() 175 | } 176 | 177 | async fn confirm_update(version: &str) -> bool { 178 | const POWERSHELL_APP_ID: &str = 179 | "{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\\WindowsPowerShell\\v1.0\\powershell.exe"; 180 | const CONFIRM_ACTION: &str = "Update"; 181 | const IGNORE_ACTION: &str = "Ignore"; 182 | 183 | let (confirm_tx, mut confirm_rx) = tokio::sync::mpsc::channel::(1); 184 | 185 | let manager = ToastManager::new(POWERSHELL_APP_ID); 186 | let mut toast = Toast::new(); 187 | toast 188 | .text1("BurntSushi") 189 | .text2(Text::new(format!("Update app to to {version}?"))) 190 | .action(Action::new("Update", CONFIRM_ACTION, CONFIRM_ACTION)) 191 | .action(Action::new("Ignore", IGNORE_ACTION, IGNORE_ACTION)); 192 | 193 | let confirm_tx2 = confirm_tx.clone(); 194 | let confirm_tx3 = confirm_tx.clone(); 195 | let confirm_tx4 = confirm_tx.clone(); 196 | 197 | let confirmed = manager.show_with_callbacks( 198 | &toast, 199 | Some(Box::new(move |res| { 200 | let confirmed = match res { 201 | Ok(arg) => { 202 | debug!("Update toast activated (arg={arg})"); 203 | arg == CONFIRM_ACTION 204 | } 205 | Err(err) => { 206 | debug!("Update toast activation failed (err={err})"); 207 | false 208 | } 209 | }; 210 | confirm_tx2.try_send(confirmed).unwrap(); 211 | })), 212 | Some(Box::new(move |res| { 213 | match res { 214 | Ok(reason) => debug!("Update toast dismissed (reason={reason:?})"), 215 | Err(err) => debug!("Update toast dismissal failed (err={err})"), 216 | } 217 | confirm_tx3.try_send(false).unwrap(); 218 | })), 219 | Some(Box::new(move |err| { 220 | error!("Update toast failed: {err}"); 221 | confirm_tx4.try_send(false).unwrap(); 222 | })), 223 | ); 224 | 225 | if let Err(err) = confirmed { 226 | error!("Failed to show toast: {err}"); 227 | return false; 228 | } 229 | 230 | confirm_rx.recv().await.unwrap() 231 | } 232 | 233 | fn download_file(url: &str, target: impl Write) -> Result<(), self_update::errors::Error> { 234 | self_update::Download::from_url(url) 235 | .set_header( 236 | reqwest::header::ACCEPT, 237 | HeaderValue::from_static("application/octet-stream"), 238 | ) 239 | .download_to(target) 240 | } 241 | -------------------------------------------------------------------------------- /burnt-sushi/wix/main.wxs: -------------------------------------------------------------------------------- 1 | 2 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | NOT Installed 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 1 111 | 112 | "1"]]> 113 | 114 | 1 115 | WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed 116 | NOT Installed 117 | Installed AND PATCH 118 | 1 119 | 1 120 | NOT WIXUI_DONTVALIDATEPATH 121 | 122 | "1"]]> 123 | 124 | WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID="1" 125 | 1 126 | 1 127 | NOT Installed 128 | Installed AND NOT PATCH 129 | Installed AND PATCH 130 | 1 131 | 1 132 | 1 133 | 1 134 | 135 | 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /burnt-sushi-blocker/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(once_cell_try)] 2 | #![warn(clippy::pedantic)] 3 | 4 | use std::{ 5 | cell::{OnceCell, RefCell}, 6 | net::{Ipv4Addr, SocketAddrV4}, 7 | rc::Rc, 8 | sync::{LazyLock, Mutex}, 9 | thread, 10 | }; 11 | 12 | use capnp::capability::Promise; 13 | use capnp_rpc::{RpcSystem, pry, rpc_twoparty_capnp, twoparty}; 14 | use dll_syringe::payload_utils::payload_procedure; 15 | use futures::{AsyncReadExt, FutureExt}; 16 | use hooks::LogParams; 17 | use regex::RegexSet; 18 | use tokio::select; 19 | 20 | #[allow(clippy::pedantic)] 21 | mod cef; 22 | mod filters; 23 | mod hooks; 24 | mod utils; 25 | pub use filters::*; 26 | 27 | static RPC_STATE: LazyLock>> = LazyLock::new(|| Mutex::new(None)); 28 | 29 | struct RpcState { 30 | rpc_thread: thread::JoinHandle<()>, 31 | rpc_disconnector: tokio::sync::watch::Sender<()>, 32 | socket_addr: SocketAddrV4, 33 | } 34 | 35 | #[payload_procedure] 36 | fn start_rpc() -> SocketAddrV4 { 37 | let mut state = RPC_STATE.lock().unwrap(); 38 | if let Some(state) = state.as_ref() { 39 | return state.socket_addr; 40 | } 41 | 42 | let (end_point_tx, end_point_rx) = tokio::sync::oneshot::channel(); 43 | let (disconnect_tx, disconnect_rx) = tokio::sync::watch::channel(()); 44 | 45 | let rpc_thread = thread::spawn(move || { 46 | tokio::runtime::Builder::new_current_thread() 47 | .enable_all() 48 | .build() 49 | .unwrap() 50 | .block_on(tokio::task::LocalSet::new().run_until(run_rpc(end_point_tx, disconnect_rx))) 51 | .unwrap(); 52 | }); 53 | 54 | let socket_addr = end_point_rx.blocking_recv().unwrap(); 55 | 56 | *state = Some(RpcState { 57 | rpc_thread, 58 | rpc_disconnector: disconnect_tx, 59 | socket_addr, 60 | }); 61 | 62 | socket_addr 63 | } 64 | 65 | #[payload_procedure] 66 | fn stop_rpc() { 67 | let mut state = RPC_STATE.lock().unwrap(); 68 | if let Some(state) = state.take() { 69 | hooks::disable().unwrap(); 70 | state.rpc_disconnector.send(()).unwrap(); 71 | state.rpc_thread.join().unwrap(); 72 | } 73 | } 74 | 75 | async fn run_rpc( 76 | end_point: tokio::sync::oneshot::Sender, 77 | mut disconnect_signal: tokio::sync::watch::Receiver<()>, 78 | ) -> Result<(), Box> { 79 | let listener = tokio::net::TcpListener::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0)).await?; 80 | end_point 81 | .send(SocketAddrV4::new( 82 | Ipv4Addr::LOCALHOST, 83 | listener.local_addr()?.port(), 84 | )) 85 | .unwrap(); 86 | let client: shared::rpc::blocker_service::Client = capnp_rpc::new_client(ServerImpl::new()); 87 | 88 | loop { 89 | select! { 90 | res = listener.accept() => { 91 | let stream = match res { 92 | Ok((stream, _)) => stream, 93 | Err(e) => return Err(e.into()), 94 | }; 95 | 96 | stream.set_nodelay(true)?; 97 | let (reader, writer) = 98 | tokio_util::compat::TokioAsyncReadCompatExt::compat(stream).split(); 99 | let network = twoparty::VatNetwork::new( 100 | reader, 101 | writer, 102 | rpc_twoparty_capnp::Side::Server, 103 | capnp::message::ReaderOptions::default(), 104 | ); 105 | 106 | let rpc_system = RpcSystem::new(Box::new(network), Some(client.clone().client)); 107 | 108 | let disconnector = rpc_system.get_disconnector(); 109 | let mut disconnect_signal = disconnect_signal.clone(); 110 | tokio::task::spawn_local(async move { 111 | disconnect_signal.changed().await.unwrap(); 112 | let _ = hooks::disable(); 113 | disconnector.await.unwrap(); 114 | }); 115 | 116 | tokio::task::spawn_local(Box::pin(rpc_system.map(|_| ()))); 117 | }, 118 | _ = disconnect_signal.changed() => { 119 | return Ok(()); 120 | } 121 | } 122 | } 123 | } 124 | 125 | #[derive(Clone)] 126 | struct LoggerManager { 127 | loggers: RefCell>, 128 | log_tx: OnceCell>, 129 | } 130 | 131 | impl LoggerManager { 132 | fn new() -> Self { 133 | Self { 134 | loggers: RefCell::new(Vec::new()), 135 | log_tx: OnceCell::new(), 136 | } 137 | } 138 | 139 | fn add_logger(&self, logger: shared::rpc::blocker_service::logger::Client) { 140 | self.loggers.borrow_mut().push(logger); 141 | } 142 | 143 | #[allow(clippy::await_holding_refcell_ref)] // Ref is dropped before await 144 | async fn log_request( 145 | &self, 146 | hook: shared::rpc::blocker_service::FilterHook, 147 | blocked: bool, 148 | url: &str, 149 | ) { 150 | let loggers = self.loggers.borrow(); 151 | let futures = futures::future::join_all(loggers.iter().map(|logger| { 152 | let mut req = logger.log_request_request(); 153 | let mut builder = req.get().init_request(); 154 | builder.set_hook(hook); 155 | builder.set_blocked(blocked); 156 | builder.set_url(url); 157 | req.send().promise 158 | })); 159 | drop(loggers); 160 | futures.await; 161 | } 162 | 163 | #[allow(clippy::await_holding_refcell_ref)] // Ref is dropped before await 164 | async fn log_message(&self, message: &str) { 165 | let loggers = self.loggers.borrow(); 166 | let futures = futures::future::join_all(loggers.iter().map(|logger| { 167 | let mut req = logger.log_message_request(); 168 | req.get().set_message(message); 169 | req.send().promise 170 | })); 171 | drop(loggers); 172 | futures.await; 173 | } 174 | 175 | fn log_sender(&self) -> tokio::sync::mpsc::UnboundedSender { 176 | self.log_tx.get_or_init(|| self.spawn_log_channel()).clone() 177 | } 178 | 179 | fn spawn_log_channel(&self) -> tokio::sync::mpsc::UnboundedSender { 180 | let this = self.clone(); 181 | let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); 182 | 183 | tokio::task::spawn_local(async move { 184 | loop { 185 | while let Some(m) = rx.recv().await { 186 | match m { 187 | LogParams::Request { hook, blocked, url } => { 188 | this.log_request(hook, blocked, &url).await; 189 | } 190 | LogParams::Message(message) => { 191 | this.log_message(&message).await; 192 | } 193 | } 194 | } 195 | } 196 | }); 197 | 198 | tx 199 | } 200 | } 201 | 202 | struct ServerImpl { 203 | logger: LoggerManager, 204 | filters: Filters, 205 | } 206 | 207 | impl ServerImpl { 208 | fn new() -> Self { 209 | Self { 210 | logger: LoggerManager::new(), 211 | filters: Filters::empty(), 212 | } 213 | } 214 | } 215 | 216 | impl shared::rpc::blocker_service::Server for ServerImpl { 217 | fn register_logger( 218 | self: Rc, 219 | params: shared::rpc::blocker_service::RegisterLoggerParams, 220 | mut _results: shared::rpc::blocker_service::RegisterLoggerResults, 221 | ) -> impl futures::Future> + 'static { 222 | self.logger 223 | .add_logger(pry!(pry!(params.get()).get_logger())); 224 | 225 | Promise::ok(()) 226 | } 227 | 228 | fn set_ruleset( 229 | self: Rc, 230 | params: shared::rpc::blocker_service::SetRulesetParams, 231 | mut _results: shared::rpc::blocker_service::SetRulesetResults, 232 | ) -> impl futures::Future> + 'static { 233 | pry!((move || { 234 | let hook = params.get()?.get_hook()?; 235 | let raw_ruleset = params.get()?.get_ruleset()?; 236 | let whitelist = raw_ruleset.get_whitelist()?; 237 | let blacklist = raw_ruleset.get_blacklist()?; 238 | 239 | let whitelist = RegexSet::new( 240 | whitelist 241 | .iter() 242 | .map(|pattern| pattern.map(|p| String::from_utf8_lossy(p.as_bytes()))) 243 | .collect::, _>>()?, 244 | ) 245 | .map_err(|e| capnp::Error::failed(e.to_string()))?; 246 | let blacklist = RegexSet::new( 247 | blacklist 248 | .iter() 249 | .map(|pattern| pattern.map(|p| String::from_utf8_lossy(p.as_bytes()))) 250 | .collect::, _>>()?, 251 | ) 252 | .map_err(|e| capnp::Error::failed(e.to_string()))?; 253 | let ruleset = FilterRuleset { 254 | whitelist, 255 | blacklist, 256 | }; 257 | self.filters.replace_ruleset(hook, ruleset); 258 | 259 | Ok::<(), capnp::Error>(()) 260 | })()); 261 | 262 | Promise::ok(()) 263 | } 264 | 265 | fn enable_filtering( 266 | self: Rc, 267 | _params: shared::rpc::blocker_service::EnableFilteringParams, 268 | mut _results: shared::rpc::blocker_service::EnableFilteringResults, 269 | ) -> impl futures::Future> + 'static { 270 | match hooks::enable(self.filters.clone(), self.logger.log_sender()) { 271 | Ok(()) => Promise::ok(()), 272 | Err(e) => Promise::err(capnp::Error::failed(e.to_string())), 273 | } 274 | } 275 | 276 | fn disable_filtering( 277 | self: Rc, 278 | _params: shared::rpc::blocker_service::DisableFilteringParams, 279 | mut _results: shared::rpc::blocker_service::DisableFilteringResults, 280 | ) -> impl futures::Future> + 'static { 281 | match hooks::disable() { 282 | Ok(()) => Promise::ok(()), 283 | Err(e) => Promise::err(capnp::Error::failed(e.to_string())), 284 | } 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /burnt-sushi/src/spotify_process_scanner.rs: -------------------------------------------------------------------------------- 1 | use dll_syringe::process::{OwnedProcess, Process}; 2 | use fallible_iterator::FallibleIterator; 3 | use log::info; 4 | use project_uninit::partial_init; 5 | use std::{ 6 | io, 7 | mem::{self, MaybeUninit}, 8 | num::{NonZeroU32, NonZeroUsize}, 9 | os::windows::prelude::{AsRawHandle, HandleOrInvalid, OwnedHandle}, 10 | ptr, 11 | }; 12 | use winapi::{ 13 | shared::{ 14 | minwindef::{BOOL, FALSE}, 15 | windef::HWND, 16 | winerror::ERROR_NO_MORE_FILES, 17 | }, 18 | um::{ 19 | errhandlingapi::{GetLastError, SetLastError}, 20 | tlhelp32::{ 21 | CreateToolhelp32Snapshot, TH32CS_SNAPTHREAD, THREADENTRY32, Thread32First, Thread32Next, 22 | }, 23 | winuser::{ 24 | EnumChildWindows, EnumThreadWindows, GetClassNameW, GetWindowTextLengthW, 25 | GetWindowTextW, GetWindowThreadProcessId, 26 | }, 27 | }, 28 | }; 29 | use wineventhook::{AccessibleObjectId, EventFilter, WindowEventHook, WindowHandle, raw_event}; 30 | 31 | #[derive(Debug)] 32 | pub struct SpotifyProcessScanner { 33 | notifier: tokio::sync::watch::Sender, 34 | } 35 | 36 | #[derive(Debug, PartialEq, Eq, Hash)] 37 | pub enum SpotifyState { 38 | Running(SpotifyInfo), 39 | Stopped, 40 | } 41 | 42 | impl SpotifyState { 43 | pub fn try_clone(&self) -> io::Result { 44 | match self { 45 | SpotifyState::Running(info) => Ok(SpotifyState::Running(info.try_clone()?)), 46 | SpotifyState::Stopped => Ok(SpotifyState::Stopped), 47 | } 48 | } 49 | } 50 | 51 | #[derive(Debug, PartialEq, Eq, Hash)] 52 | pub struct SpotifyInfo { 53 | pub process: OwnedProcess, 54 | pub main_window: WindowHandle, 55 | } 56 | 57 | unsafe impl Send for SpotifyInfo {} 58 | unsafe impl Sync for SpotifyInfo {} 59 | 60 | impl SpotifyInfo { 61 | pub fn try_clone(&self) -> io::Result { 62 | Ok(Self { 63 | process: self.process.try_clone()?, 64 | main_window: self.main_window, 65 | }) 66 | } 67 | } 68 | 69 | impl SpotifyProcessScanner { 70 | pub fn new() -> (Self, tokio::sync::watch::Receiver) { 71 | let (tx, rx) = tokio::sync::watch::channel(SpotifyState::Stopped); 72 | let scanner = Self { notifier: tx }; 73 | (scanner, rx) 74 | } 75 | 76 | #[allow(dead_code)] 77 | pub fn spawn(self) -> tokio::task::JoinHandle> { 78 | tokio::spawn(async move { self.run().await }) 79 | } 80 | 81 | pub async fn run(&self) -> io::Result<()> { 82 | self.scan()?; 83 | 84 | while !self.notifier.is_closed() { 85 | let state = self.notifier.borrow().try_clone()?; 86 | let new_state = match state { 87 | SpotifyState::Stopped => self.listen_stopped().await?, 88 | SpotifyState::Running(info) => self.listen_running(info).await?, 89 | }; 90 | 91 | if let Some(new_state) = new_state { 92 | self.change_state(new_state); 93 | } else { 94 | break; 95 | } 96 | } 97 | 98 | Ok(()) 99 | } 100 | 101 | pub fn scan(&self) -> io::Result<()> { 102 | for process in OwnedProcess::all() { 103 | if !is_spotify_process(process.borrowed()) { 104 | continue; 105 | } 106 | 107 | let mut windows = list_process_windows(process.borrowed())?; 108 | while let Some(window) = windows.next()? { 109 | if is_main_spotify_window(window) { 110 | drop(windows); 111 | self.change_state(SpotifyState::Running(SpotifyInfo { 112 | process, 113 | main_window: window, 114 | })); 115 | return Ok(()); 116 | } 117 | } 118 | } 119 | Ok(()) 120 | } 121 | 122 | fn change_state(&self, new_state: SpotifyState) { 123 | let _ = self.notifier.send(new_state); 124 | } 125 | 126 | async fn listen_stopped(&self) -> io::Result> { 127 | let (event_tx, mut event_rx) = tokio::sync::mpsc::unbounded_channel(); 128 | 129 | let event_hook = WindowEventHook::hook( 130 | EventFilter::default() 131 | .all_processes() 132 | .all_threads() 133 | .skip_own_thread(true) 134 | .skip_own_process(true) 135 | .event(raw_event::OBJECT_SHOW) 136 | .predicate(|event| { 137 | event.child_id().is_none() && event.object_type() == AccessibleObjectId::Window 138 | }), 139 | event_tx, 140 | ) 141 | .await?; 142 | 143 | while let Some(event) = event_rx.recv().await { 144 | // scoped to make future Send 145 | let state = { 146 | let Some(window) = event.window_handle() else { 147 | continue; 148 | }; 149 | let Ok(process) = get_window_process(window) else { 150 | continue; 151 | }; 152 | if !is_spotify_process(process.borrowed()) || !is_main_spotify_window(window) { 153 | continue; 154 | } 155 | 156 | SpotifyState::Running(SpotifyInfo { 157 | process, 158 | main_window: window, 159 | }) 160 | }; 161 | 162 | event_hook.unhook().await?; 163 | return Ok(Some(state)); 164 | } 165 | 166 | event_hook.unhook().await?; 167 | Ok(None) 168 | } 169 | 170 | async fn listen_running(&self, info: SpotifyInfo) -> io::Result> { 171 | let (event_tx, mut event_rx) = tokio::sync::mpsc::unbounded_channel(); 172 | 173 | let thread_id = get_window_thread_id(info.main_window); 174 | let process_id = info.process.pid()?; 175 | let event_hook = WindowEventHook::hook( 176 | EventFilter::default() 177 | .thread(thread_id) 178 | .process(process_id) 179 | .skip_own_thread(true) 180 | .skip_own_process(true) 181 | .event(raw_event::OBJECT_DESTROY) 182 | .predicate(|event| { 183 | event.child_id().is_none() && event.object_type() == AccessibleObjectId::Window 184 | }), 185 | event_tx, 186 | ) 187 | .await?; 188 | 189 | let new_state = loop { 190 | if let Some(event) = event_rx.recv().await { 191 | assert_eq!(event.thread_id(), thread_id.get()); 192 | if event.window_handle() != Some(info.main_window) { 193 | continue; 194 | } 195 | 196 | break Some(SpotifyState::Stopped); 197 | } else { 198 | break None; 199 | } 200 | }; 201 | 202 | event_hook.unhook().await?; 203 | Ok(new_state) 204 | } 205 | } 206 | 207 | fn get_window_title_length(window: WindowHandle) -> io::Result> { 208 | unsafe { SetLastError(0) }; 209 | let result = unsafe { GetWindowTextLengthW(window.as_ptr()) }; 210 | if result == 0 && unsafe { GetLastError() } != 0 { 211 | Err(io::Error::last_os_error()) 212 | } else { 213 | Ok(NonZeroUsize::new(result as usize)) 214 | } 215 | } 216 | 217 | fn get_window_title(window: WindowHandle) -> io::Result> { 218 | let text_len = if let Some(length) = get_window_title_length(window)? { 219 | length.get() 220 | } else { 221 | return Ok(None); 222 | }; 223 | 224 | let mut text = Vec::with_capacity(text_len + 1); // +1 for null terminator 225 | let result = 226 | unsafe { GetWindowTextW(window.as_ptr(), text.as_mut_ptr(), text.capacity() as i32) }; 227 | if result != 0 { 228 | unsafe { text.set_len(text_len) }; 229 | let text = String::from_utf16_lossy(&text); 230 | Ok(Some(text)) 231 | } else { 232 | Err(io::Error::last_os_error()) 233 | } 234 | } 235 | 236 | fn get_window_process(window: WindowHandle) -> io::Result { 237 | let process_id = get_window_process_id(window); 238 | OwnedProcess::from_pid(process_id.get()) 239 | } 240 | 241 | fn get_window_thread_id(window: WindowHandle) -> NonZeroU32 { 242 | let thread_id = unsafe { GetWindowThreadProcessId(window.as_ptr(), ptr::null_mut()) }; 243 | NonZeroU32::new(thread_id).unwrap() 244 | } 245 | 246 | fn get_window_process_id(window: WindowHandle) -> NonZeroU32 { 247 | let mut process_id = MaybeUninit::uninit(); 248 | let _thread_id = unsafe { GetWindowThreadProcessId(window.as_ptr(), process_id.as_mut_ptr()) }; 249 | NonZeroU32::new(unsafe { process_id.assume_init() }).unwrap() 250 | } 251 | 252 | fn is_spotify_process(process: impl Process) -> bool { 253 | match process.base_name() { 254 | Ok(mut name) => { 255 | name.make_ascii_lowercase(); 256 | name.to_string_lossy().contains("spotify") 257 | } 258 | Err(_) => false, 259 | } 260 | } 261 | 262 | fn is_main_spotify_window(window: WindowHandle) -> bool { 263 | let Ok(Some(title)) = get_window_title(window) else { 264 | return false; 265 | }; 266 | 267 | if title.trim().is_empty() || title == "G" || title == "Default IME" { 268 | return false; 269 | } 270 | 271 | let Ok(class_name) = get_window_class_name(window) else { 272 | return false; 273 | }; 274 | info!("Found window '{title}' of class '{class_name}'."); 275 | class_name.starts_with("Chrome_WidgetWin") 276 | || class_name == "Chrome_RenderWidgetHostHWND" 277 | || class_name == "GDI+ Hook Window Class" 278 | } 279 | 280 | fn get_window_class_name(window: WindowHandle) -> io::Result { 281 | let mut class_name_buf = [const { MaybeUninit::uninit() }; 256]; 282 | let result = unsafe { 283 | GetClassNameW( 284 | window.as_ptr(), 285 | class_name_buf[0].as_mut_ptr(), 286 | class_name_buf.len() as i32, 287 | ) 288 | }; 289 | match result { 290 | 0 => Err(io::Error::last_os_error()), 291 | name_len => { 292 | let name_len = name_len as usize; 293 | let class_name = unsafe { class_name_buf[..name_len].assume_init_ref() }; 294 | Ok(String::from_utf16_lossy(class_name)) 295 | } 296 | } 297 | } 298 | 299 | fn list_threads() -> io::Result> { 300 | Toolhelp32ThreadIterator::new() 301 | } 302 | 303 | fn list_process_threads( 304 | process: impl Process, 305 | ) -> io::Result> { 306 | let process_id = process.pid()?.get(); 307 | list_threads().map(move |iter| { 308 | iter.filter(move |thread| Ok(thread.th32OwnerProcessID == process_id)) 309 | .map(|thread| Ok(thread.th32ThreadID)) 310 | }) 311 | } 312 | 313 | fn list_process_windows( 314 | process: impl Process, 315 | ) -> io::Result> { 316 | list_process_threads(process).map(|iter| { 317 | iter.flat_map(|thread| { 318 | Ok(fallible_iterator::convert( 319 | list_thread_windows(thread, true).into_iter().map(Ok), 320 | )) 321 | }) 322 | }) 323 | } 324 | 325 | struct Toolhelp32ThreadIterator { 326 | snapshot: OwnedHandle, 327 | first: bool, 328 | } 329 | 330 | impl Toolhelp32ThreadIterator { 331 | pub fn new() -> io::Result { 332 | let snapshot = unsafe { 333 | CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0 /* ignored for SNAPTHREAD */) 334 | }; 335 | let snapshot = unsafe { HandleOrInvalid::from_raw_handle(snapshot) }; 336 | let snapshot: OwnedHandle = snapshot 337 | .try_into() 338 | .map_err(|_| io::Error::last_os_error())?; 339 | 340 | Ok(Toolhelp32ThreadIterator { 341 | snapshot, 342 | first: true, 343 | }) 344 | } 345 | } 346 | 347 | impl FallibleIterator for Toolhelp32ThreadIterator { 348 | type Item = THREADENTRY32; 349 | type Error = io::Error; 350 | 351 | fn next(&mut self) -> io::Result> { 352 | let mut thread = MaybeUninit::::uninit(); 353 | partial_init!(thread => { 354 | dwSize: mem::size_of::() as u32 355 | }); 356 | 357 | let result = if self.first { 358 | self.first = false; 359 | unsafe { Thread32First(self.snapshot.as_raw_handle(), thread.as_mut_ptr()) } 360 | } else { 361 | unsafe { Thread32Next(self.snapshot.as_raw_handle(), thread.as_mut_ptr()) } 362 | }; 363 | if result == FALSE { 364 | let err = io::Error::last_os_error(); 365 | if err.raw_os_error() == Some(ERROR_NO_MORE_FILES.cast_signed()) { 366 | return Ok(None); 367 | } else { 368 | return Err(err); 369 | } 370 | } 371 | 372 | let thread = unsafe { thread.assume_init() }; 373 | Ok(Some(thread)) 374 | } 375 | } 376 | 377 | fn list_thread_windows(thread_id: u32, include_children: bool) -> Vec { 378 | extern "system" fn enum_proc(window_handle: HWND, windows: isize) -> BOOL { 379 | let windows = unsafe { &mut *(windows as *mut Vec) }; 380 | windows.push(unsafe { WindowHandle::new_unchecked(window_handle) }); 381 | FALSE 382 | } 383 | 384 | let mut windows = Vec::::new(); 385 | let _result = unsafe { 386 | EnumThreadWindows( 387 | thread_id, 388 | Some(enum_proc), 389 | (&raw mut windows).addr().cast_signed(), 390 | ) 391 | }; 392 | 393 | if include_children { 394 | let root_window_count = windows.len(); 395 | for i in 0..root_window_count { 396 | let window = windows[i]; 397 | unsafe { 398 | EnumChildWindows( 399 | window.as_ptr(), 400 | Some(enum_proc), 401 | (&raw mut windows).addr().cast_signed(), 402 | ) 403 | }; 404 | } 405 | } 406 | 407 | windows 408 | } 409 | -------------------------------------------------------------------------------- /burnt-sushi-blocker/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.4" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "arc-swap" 16 | version = "1.7.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" 19 | 20 | [[package]] 21 | name = "bincode" 22 | version = "2.0.1" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" 25 | dependencies = [ 26 | "serde", 27 | "unty", 28 | ] 29 | 30 | [[package]] 31 | name = "bitflags" 32 | version = "1.3.2" 33 | source = "registry+https://github.com/rust-lang/crates.io-index" 34 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 35 | 36 | [[package]] 37 | name = "bitflags" 38 | version = "2.10.0" 39 | source = "registry+https://github.com/rust-lang/crates.io-index" 40 | checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" 41 | 42 | [[package]] 43 | name = "burnt-sushi-blocker" 44 | version = "0.3.2" 45 | dependencies = [ 46 | "arc-swap", 47 | "capnp", 48 | "capnp-rpc", 49 | "dll-syringe", 50 | "enum-map", 51 | "futures", 52 | "regex", 53 | "retour", 54 | "shared", 55 | "tokio", 56 | "tokio-util", 57 | "winapi", 58 | ] 59 | 60 | [[package]] 61 | name = "bytes" 62 | version = "1.11.0" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" 65 | 66 | [[package]] 67 | name = "capnp" 68 | version = "0.23.0" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "bbffb6e69e1ea2cf518ac5037a78a223ecb82b4ac81e7ab33cb5211b43d712ac" 71 | dependencies = [ 72 | "embedded-io", 73 | ] 74 | 75 | [[package]] 76 | name = "capnp-futures" 77 | version = "0.23.0" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "a5ba7ae93ff689252798d8c71274512738ac2f3993da2adc961aa7441d2a27e6" 80 | dependencies = [ 81 | "capnp", 82 | "futures-channel", 83 | "futures-util", 84 | ] 85 | 86 | [[package]] 87 | name = "capnp-rpc" 88 | version = "0.23.0" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "c263a9de8bfe8a151e30f6d32e620483dcdca4ed8fed6913be9bd9803cbd4e5e" 91 | dependencies = [ 92 | "capnp", 93 | "capnp-futures", 94 | "futures", 95 | ] 96 | 97 | [[package]] 98 | name = "capnpc" 99 | version = "0.23.2" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "3f1cd4f042d6725da6245bde08c9e7c74f6fcb2d8bd5e0378e57991be8d711d8" 102 | dependencies = [ 103 | "capnp", 104 | ] 105 | 106 | [[package]] 107 | name = "cargo-emit" 108 | version = "0.2.1" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "1582e1c9e755dd6ad6b224dcffb135d199399a4568d454bd89fe515ca8425695" 111 | 112 | [[package]] 113 | name = "cfg-if" 114 | version = "1.0.4" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 117 | 118 | [[package]] 119 | name = "const_panic" 120 | version = "0.2.15" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "e262cdaac42494e3ae34c43969f9cdeb7da178bdb4b66fa6a1ea2edb4c8ae652" 123 | dependencies = [ 124 | "typewit", 125 | ] 126 | 127 | [[package]] 128 | name = "dll-syringe" 129 | version = "0.17.1" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "a7048876a2194fb2f949fc7e99bf0adc7c32acebb83e99c719658a0f08b28b6c" 132 | dependencies = [ 133 | "bincode", 134 | "dll-syringe-macros", 135 | "goblin", 136 | "iced-x86", 137 | "konst", 138 | "num_enum", 139 | "path-absolutize", 140 | "same-file", 141 | "serde", 142 | "shrinkwraprs", 143 | "stopwatch2", 144 | "sysinfo", 145 | "thiserror", 146 | "widestring", 147 | "winapi", 148 | ] 149 | 150 | [[package]] 151 | name = "dll-syringe-macros" 152 | version = "0.1.0" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "3a52aadbd0973e4db2d0781f869c38794ffa03dde3b46eb2fc302a7e6feb5103" 155 | dependencies = [ 156 | "quote", 157 | "syn 2.0.111", 158 | ] 159 | 160 | [[package]] 161 | name = "either" 162 | version = "1.15.0" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 165 | 166 | [[package]] 167 | name = "embedded-io" 168 | version = "0.7.1" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "9eb1aa714776b75c7e67e1da744b81a129b3ff919c8712b5e1b32252c1f07cc7" 171 | 172 | [[package]] 173 | name = "enum-map" 174 | version = "2.7.3" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" 177 | dependencies = [ 178 | "enum-map-derive", 179 | ] 180 | 181 | [[package]] 182 | name = "enum-map-derive" 183 | version = "0.17.0" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" 186 | dependencies = [ 187 | "proc-macro2", 188 | "quote", 189 | "syn 2.0.111", 190 | ] 191 | 192 | [[package]] 193 | name = "futures" 194 | version = "0.3.31" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 197 | dependencies = [ 198 | "futures-channel", 199 | "futures-core", 200 | "futures-io", 201 | "futures-sink", 202 | "futures-task", 203 | "futures-util", 204 | ] 205 | 206 | [[package]] 207 | name = "futures-channel" 208 | version = "0.3.31" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 211 | dependencies = [ 212 | "futures-core", 213 | "futures-sink", 214 | ] 215 | 216 | [[package]] 217 | name = "futures-core" 218 | version = "0.3.31" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 221 | 222 | [[package]] 223 | name = "futures-io" 224 | version = "0.3.31" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 227 | 228 | [[package]] 229 | name = "futures-sink" 230 | version = "0.3.31" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 233 | 234 | [[package]] 235 | name = "futures-task" 236 | version = "0.3.31" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 239 | 240 | [[package]] 241 | name = "futures-util" 242 | version = "0.3.31" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 245 | dependencies = [ 246 | "futures-channel", 247 | "futures-core", 248 | "futures-io", 249 | "futures-sink", 250 | "futures-task", 251 | "memchr", 252 | "pin-project-lite", 253 | "pin-utils", 254 | "slab", 255 | ] 256 | 257 | [[package]] 258 | name = "generic-array" 259 | version = "0.14.9" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" 262 | dependencies = [ 263 | "typenum", 264 | "version_check", 265 | ] 266 | 267 | [[package]] 268 | name = "goblin" 269 | version = "0.10.4" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "4db6758c546e6f81f265638c980e5e84dfbda80cfd8e89e02f83454c8e8124bd" 272 | dependencies = [ 273 | "log", 274 | "plain", 275 | "scroll", 276 | ] 277 | 278 | [[package]] 279 | name = "iced-x86" 280 | version = "1.21.0" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "7c447cff8c7f384a7d4f741cfcff32f75f3ad02b406432e8d6c878d56b1edf6b" 283 | dependencies = [ 284 | "lazy_static", 285 | ] 286 | 287 | [[package]] 288 | name = "itertools" 289 | version = "0.8.2" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" 292 | dependencies = [ 293 | "either", 294 | ] 295 | 296 | [[package]] 297 | name = "konst" 298 | version = "0.4.3" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "f660d5f887e3562f9ab6f4a14988795b694099d66b4f5dedc02d197ba9becb1d" 301 | dependencies = [ 302 | "const_panic", 303 | "typewit", 304 | ] 305 | 306 | [[package]] 307 | name = "lazy_static" 308 | version = "1.5.0" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 311 | 312 | [[package]] 313 | name = "libc" 314 | version = "0.2.178" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" 317 | 318 | [[package]] 319 | name = "log" 320 | version = "0.4.29" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" 323 | 324 | [[package]] 325 | name = "mach2" 326 | version = "0.4.3" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" 329 | dependencies = [ 330 | "libc", 331 | ] 332 | 333 | [[package]] 334 | name = "memchr" 335 | version = "2.7.6" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" 338 | 339 | [[package]] 340 | name = "mio" 341 | version = "1.1.1" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" 344 | dependencies = [ 345 | "libc", 346 | "wasi", 347 | "windows-sys 0.61.2", 348 | ] 349 | 350 | [[package]] 351 | name = "mmap-fixed-fixed" 352 | version = "0.1.3" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "0681853891801e4763dc252e843672faf32bcfee27a0aa3b19733902af450acc" 355 | dependencies = [ 356 | "libc", 357 | "winapi", 358 | ] 359 | 360 | [[package]] 361 | name = "ntapi" 362 | version = "0.4.1" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" 365 | dependencies = [ 366 | "winapi", 367 | ] 368 | 369 | [[package]] 370 | name = "num_enum" 371 | version = "0.7.5" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" 374 | dependencies = [ 375 | "num_enum_derive", 376 | "rustversion", 377 | ] 378 | 379 | [[package]] 380 | name = "num_enum_derive" 381 | version = "0.7.5" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" 384 | dependencies = [ 385 | "proc-macro2", 386 | "quote", 387 | "syn 2.0.111", 388 | ] 389 | 390 | [[package]] 391 | name = "objc2-core-foundation" 392 | version = "0.3.2" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" 395 | dependencies = [ 396 | "bitflags 2.10.0", 397 | ] 398 | 399 | [[package]] 400 | name = "objc2-io-kit" 401 | version = "0.3.2" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" 404 | dependencies = [ 405 | "libc", 406 | "objc2-core-foundation", 407 | ] 408 | 409 | [[package]] 410 | name = "once_cell" 411 | version = "1.21.3" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 414 | 415 | [[package]] 416 | name = "path-absolutize" 417 | version = "3.1.1" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "e4af381fe79fa195b4909485d99f73a80792331df0625188e707854f0b3383f5" 420 | dependencies = [ 421 | "path-dedot", 422 | ] 423 | 424 | [[package]] 425 | name = "path-dedot" 426 | version = "3.1.1" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "07ba0ad7e047712414213ff67533e6dd477af0a4e1d14fb52343e53d30ea9397" 429 | dependencies = [ 430 | "once_cell", 431 | ] 432 | 433 | [[package]] 434 | name = "pin-project-lite" 435 | version = "0.2.16" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 438 | 439 | [[package]] 440 | name = "pin-utils" 441 | version = "0.1.0" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 444 | 445 | [[package]] 446 | name = "plain" 447 | version = "0.2.3" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" 450 | 451 | [[package]] 452 | name = "proc-macro2" 453 | version = "1.0.103" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" 456 | dependencies = [ 457 | "unicode-ident", 458 | ] 459 | 460 | [[package]] 461 | name = "quote" 462 | version = "1.0.42" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" 465 | dependencies = [ 466 | "proc-macro2", 467 | ] 468 | 469 | [[package]] 470 | name = "regex" 471 | version = "1.12.2" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" 474 | dependencies = [ 475 | "aho-corasick", 476 | "memchr", 477 | "regex-automata", 478 | "regex-syntax", 479 | ] 480 | 481 | [[package]] 482 | name = "regex-automata" 483 | version = "0.4.13" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" 486 | dependencies = [ 487 | "aho-corasick", 488 | "memchr", 489 | "regex-syntax", 490 | ] 491 | 492 | [[package]] 493 | name = "regex-syntax" 494 | version = "0.8.8" 495 | source = "registry+https://github.com/rust-lang/crates.io-index" 496 | checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" 497 | 498 | [[package]] 499 | name = "region" 500 | version = "3.0.2" 501 | source = "registry+https://github.com/rust-lang/crates.io-index" 502 | checksum = "e6b6ebd13bc009aef9cd476c1310d49ac354d36e240cf1bd753290f3dc7199a7" 503 | dependencies = [ 504 | "bitflags 1.3.2", 505 | "libc", 506 | "mach2", 507 | "windows-sys 0.52.0", 508 | ] 509 | 510 | [[package]] 511 | name = "retour" 512 | version = "0.4.0-alpha.4" 513 | source = "registry+https://github.com/rust-lang/crates.io-index" 514 | checksum = "ead4bc8e12d553ff70769c5f5c21f5f4f0e73c0018068a6bb5a3d7d3b9e57ec7" 515 | dependencies = [ 516 | "cfg-if", 517 | "generic-array", 518 | "iced-x86", 519 | "libc", 520 | "mmap-fixed-fixed", 521 | "once_cell", 522 | "region", 523 | "slice-pool2", 524 | ] 525 | 526 | [[package]] 527 | name = "rustversion" 528 | version = "1.0.22" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" 531 | 532 | [[package]] 533 | name = "same-file" 534 | version = "1.0.6" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 537 | dependencies = [ 538 | "winapi-util", 539 | ] 540 | 541 | [[package]] 542 | name = "scroll" 543 | version = "0.13.0" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "c1257cd4248b4132760d6524d6dda4e053bc648c9070b960929bf50cfb1e7add" 546 | dependencies = [ 547 | "scroll_derive", 548 | ] 549 | 550 | [[package]] 551 | name = "scroll_derive" 552 | version = "0.13.1" 553 | source = "registry+https://github.com/rust-lang/crates.io-index" 554 | checksum = "ed76efe62313ab6610570951494bdaa81568026e0318eaa55f167de70eeea67d" 555 | dependencies = [ 556 | "proc-macro2", 557 | "quote", 558 | "syn 2.0.111", 559 | ] 560 | 561 | [[package]] 562 | name = "serde" 563 | version = "1.0.228" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 566 | dependencies = [ 567 | "serde_core", 568 | ] 569 | 570 | [[package]] 571 | name = "serde_core" 572 | version = "1.0.228" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 575 | dependencies = [ 576 | "serde_derive", 577 | ] 578 | 579 | [[package]] 580 | name = "serde_derive" 581 | version = "1.0.228" 582 | source = "registry+https://github.com/rust-lang/crates.io-index" 583 | checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 584 | dependencies = [ 585 | "proc-macro2", 586 | "quote", 587 | "syn 2.0.111", 588 | ] 589 | 590 | [[package]] 591 | name = "shared" 592 | version = "0.3.2" 593 | dependencies = [ 594 | "capnp", 595 | "capnpc", 596 | "cargo-emit", 597 | "enum-map", 598 | "regex", 599 | ] 600 | 601 | [[package]] 602 | name = "shrinkwraprs" 603 | version = "0.3.0" 604 | source = "registry+https://github.com/rust-lang/crates.io-index" 605 | checksum = "e63e6744142336dfb606fe2b068afa2e1cca1ee6a5d8377277a92945d81fa331" 606 | dependencies = [ 607 | "bitflags 1.3.2", 608 | "itertools", 609 | "proc-macro2", 610 | "quote", 611 | "syn 1.0.109", 612 | ] 613 | 614 | [[package]] 615 | name = "slab" 616 | version = "0.4.11" 617 | source = "registry+https://github.com/rust-lang/crates.io-index" 618 | checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" 619 | 620 | [[package]] 621 | name = "slice-pool2" 622 | version = "0.4.3" 623 | source = "registry+https://github.com/rust-lang/crates.io-index" 624 | checksum = "7a3d689654af89bdfeba29a914ab6ac0236d382eb3b764f7454dde052f2821f8" 625 | 626 | [[package]] 627 | name = "socket2" 628 | version = "0.6.1" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" 631 | dependencies = [ 632 | "libc", 633 | "windows-sys 0.60.2", 634 | ] 635 | 636 | [[package]] 637 | name = "stopwatch2" 638 | version = "2.0.0" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "911ece10388afa48417f99e01df038460b6249a3ee0255f6446a6881b702fbb4" 641 | 642 | [[package]] 643 | name = "syn" 644 | version = "1.0.109" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 647 | dependencies = [ 648 | "proc-macro2", 649 | "quote", 650 | "unicode-ident", 651 | ] 652 | 653 | [[package]] 654 | name = "syn" 655 | version = "2.0.111" 656 | source = "registry+https://github.com/rust-lang/crates.io-index" 657 | checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" 658 | dependencies = [ 659 | "proc-macro2", 660 | "quote", 661 | "unicode-ident", 662 | ] 663 | 664 | [[package]] 665 | name = "sysinfo" 666 | version = "0.37.2" 667 | source = "registry+https://github.com/rust-lang/crates.io-index" 668 | checksum = "16607d5caffd1c07ce073528f9ed972d88db15dd44023fa57142963be3feb11f" 669 | dependencies = [ 670 | "libc", 671 | "memchr", 672 | "ntapi", 673 | "objc2-core-foundation", 674 | "objc2-io-kit", 675 | "windows", 676 | ] 677 | 678 | [[package]] 679 | name = "thiserror" 680 | version = "2.0.17" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" 683 | dependencies = [ 684 | "thiserror-impl", 685 | ] 686 | 687 | [[package]] 688 | name = "thiserror-impl" 689 | version = "2.0.17" 690 | source = "registry+https://github.com/rust-lang/crates.io-index" 691 | checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" 692 | dependencies = [ 693 | "proc-macro2", 694 | "quote", 695 | "syn 2.0.111", 696 | ] 697 | 698 | [[package]] 699 | name = "tokio" 700 | version = "1.48.0" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" 703 | dependencies = [ 704 | "libc", 705 | "mio", 706 | "pin-project-lite", 707 | "socket2", 708 | "tokio-macros", 709 | "windows-sys 0.61.2", 710 | ] 711 | 712 | [[package]] 713 | name = "tokio-macros" 714 | version = "2.6.0" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" 717 | dependencies = [ 718 | "proc-macro2", 719 | "quote", 720 | "syn 2.0.111", 721 | ] 722 | 723 | [[package]] 724 | name = "tokio-util" 725 | version = "0.7.17" 726 | source = "registry+https://github.com/rust-lang/crates.io-index" 727 | checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" 728 | dependencies = [ 729 | "bytes", 730 | "futures-core", 731 | "futures-io", 732 | "futures-sink", 733 | "pin-project-lite", 734 | "tokio", 735 | ] 736 | 737 | [[package]] 738 | name = "typenum" 739 | version = "1.19.0" 740 | source = "registry+https://github.com/rust-lang/crates.io-index" 741 | checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" 742 | 743 | [[package]] 744 | name = "typewit" 745 | version = "1.14.2" 746 | source = "registry+https://github.com/rust-lang/crates.io-index" 747 | checksum = "f8c1ae7cc0fdb8b842d65d127cb981574b0d2b249b74d1c7a2986863dc134f71" 748 | 749 | [[package]] 750 | name = "unicode-ident" 751 | version = "1.0.22" 752 | source = "registry+https://github.com/rust-lang/crates.io-index" 753 | checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" 754 | 755 | [[package]] 756 | name = "unty" 757 | version = "0.0.4" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" 760 | 761 | [[package]] 762 | name = "version_check" 763 | version = "0.9.5" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 766 | 767 | [[package]] 768 | name = "wasi" 769 | version = "0.11.1+wasi-snapshot-preview1" 770 | source = "registry+https://github.com/rust-lang/crates.io-index" 771 | checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 772 | 773 | [[package]] 774 | name = "widestring" 775 | version = "1.2.1" 776 | source = "registry+https://github.com/rust-lang/crates.io-index" 777 | checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" 778 | 779 | [[package]] 780 | name = "winapi" 781 | version = "0.3.9" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 784 | dependencies = [ 785 | "winapi-i686-pc-windows-gnu", 786 | "winapi-x86_64-pc-windows-gnu", 787 | ] 788 | 789 | [[package]] 790 | name = "winapi-i686-pc-windows-gnu" 791 | version = "0.4.0" 792 | source = "registry+https://github.com/rust-lang/crates.io-index" 793 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 794 | 795 | [[package]] 796 | name = "winapi-util" 797 | version = "0.1.11" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" 800 | dependencies = [ 801 | "windows-sys 0.61.2", 802 | ] 803 | 804 | [[package]] 805 | name = "winapi-x86_64-pc-windows-gnu" 806 | version = "0.4.0" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 809 | 810 | [[package]] 811 | name = "windows" 812 | version = "0.61.3" 813 | source = "registry+https://github.com/rust-lang/crates.io-index" 814 | checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" 815 | dependencies = [ 816 | "windows-collections", 817 | "windows-core", 818 | "windows-future", 819 | "windows-link 0.1.3", 820 | "windows-numerics", 821 | ] 822 | 823 | [[package]] 824 | name = "windows-collections" 825 | version = "0.2.0" 826 | source = "registry+https://github.com/rust-lang/crates.io-index" 827 | checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" 828 | dependencies = [ 829 | "windows-core", 830 | ] 831 | 832 | [[package]] 833 | name = "windows-core" 834 | version = "0.61.2" 835 | source = "registry+https://github.com/rust-lang/crates.io-index" 836 | checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" 837 | dependencies = [ 838 | "windows-implement", 839 | "windows-interface", 840 | "windows-link 0.1.3", 841 | "windows-result", 842 | "windows-strings", 843 | ] 844 | 845 | [[package]] 846 | name = "windows-future" 847 | version = "0.2.1" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" 850 | dependencies = [ 851 | "windows-core", 852 | "windows-link 0.1.3", 853 | "windows-threading", 854 | ] 855 | 856 | [[package]] 857 | name = "windows-implement" 858 | version = "0.60.2" 859 | source = "registry+https://github.com/rust-lang/crates.io-index" 860 | checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" 861 | dependencies = [ 862 | "proc-macro2", 863 | "quote", 864 | "syn 2.0.111", 865 | ] 866 | 867 | [[package]] 868 | name = "windows-interface" 869 | version = "0.59.3" 870 | source = "registry+https://github.com/rust-lang/crates.io-index" 871 | checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" 872 | dependencies = [ 873 | "proc-macro2", 874 | "quote", 875 | "syn 2.0.111", 876 | ] 877 | 878 | [[package]] 879 | name = "windows-link" 880 | version = "0.1.3" 881 | source = "registry+https://github.com/rust-lang/crates.io-index" 882 | checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" 883 | 884 | [[package]] 885 | name = "windows-link" 886 | version = "0.2.1" 887 | source = "registry+https://github.com/rust-lang/crates.io-index" 888 | checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 889 | 890 | [[package]] 891 | name = "windows-numerics" 892 | version = "0.2.0" 893 | source = "registry+https://github.com/rust-lang/crates.io-index" 894 | checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" 895 | dependencies = [ 896 | "windows-core", 897 | "windows-link 0.1.3", 898 | ] 899 | 900 | [[package]] 901 | name = "windows-result" 902 | version = "0.3.4" 903 | source = "registry+https://github.com/rust-lang/crates.io-index" 904 | checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" 905 | dependencies = [ 906 | "windows-link 0.1.3", 907 | ] 908 | 909 | [[package]] 910 | name = "windows-strings" 911 | version = "0.4.2" 912 | source = "registry+https://github.com/rust-lang/crates.io-index" 913 | checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" 914 | dependencies = [ 915 | "windows-link 0.1.3", 916 | ] 917 | 918 | [[package]] 919 | name = "windows-sys" 920 | version = "0.52.0" 921 | source = "registry+https://github.com/rust-lang/crates.io-index" 922 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 923 | dependencies = [ 924 | "windows-targets 0.52.6", 925 | ] 926 | 927 | [[package]] 928 | name = "windows-sys" 929 | version = "0.60.2" 930 | source = "registry+https://github.com/rust-lang/crates.io-index" 931 | checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 932 | dependencies = [ 933 | "windows-targets 0.53.5", 934 | ] 935 | 936 | [[package]] 937 | name = "windows-sys" 938 | version = "0.61.2" 939 | source = "registry+https://github.com/rust-lang/crates.io-index" 940 | checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" 941 | dependencies = [ 942 | "windows-link 0.2.1", 943 | ] 944 | 945 | [[package]] 946 | name = "windows-targets" 947 | version = "0.52.6" 948 | source = "registry+https://github.com/rust-lang/crates.io-index" 949 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 950 | dependencies = [ 951 | "windows_aarch64_gnullvm 0.52.6", 952 | "windows_aarch64_msvc 0.52.6", 953 | "windows_i686_gnu 0.52.6", 954 | "windows_i686_gnullvm 0.52.6", 955 | "windows_i686_msvc 0.52.6", 956 | "windows_x86_64_gnu 0.52.6", 957 | "windows_x86_64_gnullvm 0.52.6", 958 | "windows_x86_64_msvc 0.52.6", 959 | ] 960 | 961 | [[package]] 962 | name = "windows-targets" 963 | version = "0.53.5" 964 | source = "registry+https://github.com/rust-lang/crates.io-index" 965 | checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" 966 | dependencies = [ 967 | "windows-link 0.2.1", 968 | "windows_aarch64_gnullvm 0.53.1", 969 | "windows_aarch64_msvc 0.53.1", 970 | "windows_i686_gnu 0.53.1", 971 | "windows_i686_gnullvm 0.53.1", 972 | "windows_i686_msvc 0.53.1", 973 | "windows_x86_64_gnu 0.53.1", 974 | "windows_x86_64_gnullvm 0.53.1", 975 | "windows_x86_64_msvc 0.53.1", 976 | ] 977 | 978 | [[package]] 979 | name = "windows-threading" 980 | version = "0.1.0" 981 | source = "registry+https://github.com/rust-lang/crates.io-index" 982 | checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" 983 | dependencies = [ 984 | "windows-link 0.1.3", 985 | ] 986 | 987 | [[package]] 988 | name = "windows_aarch64_gnullvm" 989 | version = "0.52.6" 990 | source = "registry+https://github.com/rust-lang/crates.io-index" 991 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 992 | 993 | [[package]] 994 | name = "windows_aarch64_gnullvm" 995 | version = "0.53.1" 996 | source = "registry+https://github.com/rust-lang/crates.io-index" 997 | checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" 998 | 999 | [[package]] 1000 | name = "windows_aarch64_msvc" 1001 | version = "0.52.6" 1002 | source = "registry+https://github.com/rust-lang/crates.io-index" 1003 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1004 | 1005 | [[package]] 1006 | name = "windows_aarch64_msvc" 1007 | version = "0.53.1" 1008 | source = "registry+https://github.com/rust-lang/crates.io-index" 1009 | checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" 1010 | 1011 | [[package]] 1012 | name = "windows_i686_gnu" 1013 | version = "0.52.6" 1014 | source = "registry+https://github.com/rust-lang/crates.io-index" 1015 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1016 | 1017 | [[package]] 1018 | name = "windows_i686_gnu" 1019 | version = "0.53.1" 1020 | source = "registry+https://github.com/rust-lang/crates.io-index" 1021 | checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" 1022 | 1023 | [[package]] 1024 | name = "windows_i686_gnullvm" 1025 | version = "0.52.6" 1026 | source = "registry+https://github.com/rust-lang/crates.io-index" 1027 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1028 | 1029 | [[package]] 1030 | name = "windows_i686_gnullvm" 1031 | version = "0.53.1" 1032 | source = "registry+https://github.com/rust-lang/crates.io-index" 1033 | checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" 1034 | 1035 | [[package]] 1036 | name = "windows_i686_msvc" 1037 | version = "0.52.6" 1038 | source = "registry+https://github.com/rust-lang/crates.io-index" 1039 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1040 | 1041 | [[package]] 1042 | name = "windows_i686_msvc" 1043 | version = "0.53.1" 1044 | source = "registry+https://github.com/rust-lang/crates.io-index" 1045 | checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" 1046 | 1047 | [[package]] 1048 | name = "windows_x86_64_gnu" 1049 | version = "0.52.6" 1050 | source = "registry+https://github.com/rust-lang/crates.io-index" 1051 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1052 | 1053 | [[package]] 1054 | name = "windows_x86_64_gnu" 1055 | version = "0.53.1" 1056 | source = "registry+https://github.com/rust-lang/crates.io-index" 1057 | checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" 1058 | 1059 | [[package]] 1060 | name = "windows_x86_64_gnullvm" 1061 | version = "0.52.6" 1062 | source = "registry+https://github.com/rust-lang/crates.io-index" 1063 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1064 | 1065 | [[package]] 1066 | name = "windows_x86_64_gnullvm" 1067 | version = "0.53.1" 1068 | source = "registry+https://github.com/rust-lang/crates.io-index" 1069 | checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" 1070 | 1071 | [[package]] 1072 | name = "windows_x86_64_msvc" 1073 | version = "0.52.6" 1074 | source = "registry+https://github.com/rust-lang/crates.io-index" 1075 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1076 | 1077 | [[package]] 1078 | name = "windows_x86_64_msvc" 1079 | version = "0.53.1" 1080 | source = "registry+https://github.com/rust-lang/crates.io-index" 1081 | checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" 1082 | --------------------------------------------------------------------------------