diff --git a/.cargo/config.toml b/.cargo/config.toml index 4acb8e5..9380186 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,5 +1,2 @@ [build] target = "i686-pc-windows-msvc" - -[target.i686-pc-windows-msvc] -rustflags = ["-C", "target-feature=+crt-static"] diff --git a/.changes/config.json b/.changes/config.json index 72cfc9f..de57d3a 100644 --- a/.changes/config.json +++ b/.changes/config.json @@ -44,16 +44,6 @@ } ] }, - "nsis_download": { - "path": "./crates/nsis-download", - "manager": "rust", - "assets": [ - { - "path": "target/i686-pc-windows-msvc/release/${ pkg.pkg }.dll", - "name": "${ pkg.pkg }.dll" - } - ] - }, "nsis_process": { "path": "./crates/nsis-process", "manager": "rust", diff --git a/.changes/no_std.md b/.changes/no_std.md new file mode 100644 index 0000000..c8849b8 --- /dev/null +++ b/.changes/no_std.md @@ -0,0 +1,7 @@ +--- +"nsis_tauri_utils": "minor" +"nsis_semvercompare": "minor" +"nsis_process": "minor" +--- + +Reduce the DLL size by using `no_std` and without static msvcrt. diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 184b5af..d2531d1 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -27,8 +27,7 @@ jobs: audit: runs-on: windows-latest steps: - - uses: actions/checkout@v2 - - name: rust audit - uses: actions-rs/audit-check@v1 + - uses: actions/checkout@v4 + - uses: rustsec/audit-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/change-status-on-pr.yml b/.github/workflows/change-status-on-pr.yml index 44f842c..e0d84df 100644 --- a/.github/workflows/change-status-on-pr.yml +++ b/.github/workflows/change-status-on-pr.yml @@ -10,7 +10,7 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: covector status diff --git a/.github/workflows/clippy-fmt.yml b/.github/workflows/clippy-fmt.yml index c72d413..377629d 100644 --- a/.github/workflows/clippy-fmt.yml +++ b/.github/workflows/clippy-fmt.yml @@ -18,33 +18,17 @@ jobs: clippy: runs-on: windows-latest steps: - - uses: actions/checkout@v3 - - name: install stable - uses: actions-rs/toolchain@v1 + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable with: - profile: minimal - toolchain: stable - override: true components: clippy + - run: cargo clippy --release --all-targets --all-features -- -D warnings - - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --all-targets --all-features -- -D warnings - - fmt: + rustfmt: runs-on: windows-latest steps: - - uses: actions/checkout@v3 - - name: install stable - uses: actions-rs/toolchain@v1 + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable with: - profile: minimal - toolchain: stable - override: true components: rustfmt - - - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all -- --check \ No newline at end of file + - run: cargo fmt --all -- --check \ No newline at end of file diff --git a/.github/workflows/covector-version-or-publish.yml b/.github/workflows/covector-version-or-publish.yml index b301a21..a0cce04 100644 --- a/.github/workflows/covector-version-or-publish.yml +++ b/.github/workflows/covector-version-or-publish.yml @@ -19,16 +19,11 @@ jobs: successfulPublish: ${{ steps.covector.outputs.successfulPublish }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Setup node - uses: actions/setup-node@v3 - with: - node-version: 16 - check-latest: true - registry-url: 'https://registry.npmjs.org' + - uses: actions/setup-node@v4 - name: git config run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 55cbb3e..cb7f712 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,15 +22,6 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v2 - - - name: install stable - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - - - uses: actions-rs/cargo@v1 - with: - command: test \ No newline at end of file + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - run: cargo test --features test \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index a299928..c6d62d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" license = "MIT or Apache-2.0" [workspace.dependencies] -pluginapi = { path = "./crates/pluginapi" } +nsis-plugin-api = { path = "./crates/nsis-plugin-api" } [workspace.dependencies.windows-sys] version = "0.52.0" @@ -24,8 +24,8 @@ features = [ ] [profile.release] -codegen-units = 1 -lto = true -opt-level = "s" -panic = "abort" -strip = "symbols" +panic = "abort" # Strip expensive panic clean-up logic +codegen-units = 1 # Compile crates one after another so the compiler can optimize better +lto = true # Enables link time optimizations +opt-level = "s" # Optimize for binary size +strip = true # Remove debug symbols diff --git a/README.md b/README.md index d2993ec..673a6ae 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,12 @@ A collection of NSIS plugins written in rust. -| Plugin | Description | -|---|---| -| [nsis-download](./crates/nsis-download/) | Download a file from an URL to a path | -| [nsis-process](./crates/nsis-process/) | Find and Kill processes | -| [nsis-semvercompare](./crates/nsis-semvercompare/) | Compare two semantic versions | -| [nsis-tauri-utils](./crates/nsis-tauri-utils/) | A collection of all the above plugins into a single DLL for smaller size | +| Plugin | Description | +| -------------------------------------------------- | ------------------------------------------------------------------------ | +| [nsis-process](./crates/nsis-process/) | Find and Kill processes | +| [nsis-semvercompare](./crates/nsis-semvercompare/) | Compare two semantic versions | +| [nsis-tauri-utils](./crates/nsis-tauri-utils/) | A collection of all the above plugins into a single DLL for smaller size | ## License -Apache-2.0/MIT \ No newline at end of file +Apache-2.0/MIT diff --git a/crates/nsis-download/CHANGELOG.md b/crates/nsis-download/CHANGELOG.md deleted file mode 100644 index 4ff477a..0000000 --- a/crates/nsis-download/CHANGELOG.md +++ /dev/null @@ -1,16 +0,0 @@ -# Changelog - -## \[0.3.0] - -- [`33ea4bc`](https://www.github.com/tauri-apps/nsis-tauri-utils/commit/33ea4bcf2a573461ebc5181ef2921d8746005049)([#17](https://www.github.com/tauri-apps/nsis-tauri-utils/pull/17)) Statically link CRT. - -## \[0.2.0] - -- Add download progress bar - - [eba1392](https://www.github.com/tauri-apps/nsis-tauri-utils/commit/eba1392081d22879383ba1e21c6b7bceb19a42f2) feat(download): add progress bar ([#8](https://www.github.com/tauri-apps/nsis-tauri-utils/pull/8)) on 2023-01-24 - - [f048814](https://www.github.com/tauri-apps/nsis-tauri-utils/commit/f048814ba73b0f7436e9e25bb9cb0885e8e05fef) chore: update bump to minor on 2023-01-24 - -## \[0.1.0] - -- Initial Release. - - [000d632](https://www.github.com/tauri-apps/nsis-tauri-utils/commit/000d6326333f862741f1514de34542316445951e) ci: setup CI/CD and covector ([#2](https://www.github.com/tauri-apps/nsis-tauri-utils/pull/2)) on 2023-01-21 diff --git a/crates/nsis-download/Cargo.toml b/crates/nsis-download/Cargo.toml deleted file mode 100644 index e26069e..0000000 --- a/crates/nsis-download/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "nsis-download" -version = "0.3.0" -authors = { workspace = true } -edition = { workspace = true } -license = { workspace = true } - -[lib] -crate-type = ["rlib", "cdylib"] - -[dependencies] -ureq = { version = "2", default-features = false, features = ["tls"] } -progress-streams = "1.1" -pluginapi = { workspace = true } -windows-sys = { workspace = true } diff --git a/crates/nsis-download/src/lib.rs b/crates/nsis-download/src/lib.rs deleted file mode 100644 index a4103a1..0000000 --- a/crates/nsis-download/src/lib.rs +++ /dev/null @@ -1,204 +0,0 @@ -use std::{fs, io, path::Path}; - -use pluginapi::{exdll_init, popstring, pushint, stack_t, wchar_t}; -use progress_streams::ProgressReader; -use windows_sys::Win32::{ - Foundation::HWND, - UI::{ - Controls::{PBM_SETPOS, PROGRESS_CLASSW, WC_STATICW}, - WindowsAndMessaging::{ - CreateWindowExW, FindWindowExW, GetWindowLongPtrW, SendMessageW, SetWindowPos, - SetWindowTextW, ShowWindow, GWL_STYLE, SWP_FRAMECHANGED, SWP_NOSIZE, SW_HIDE, - WM_GETFONT, WM_SETFONT, WS_CHILD, WS_VISIBLE, - }, - }, -}; - -/// Download a file from an URL to a path. -/// -/// # Safety -/// -/// This function always expects 2 strings on the stack ($1: url, $2: path) and will panic otherwise. -#[no_mangle] -pub unsafe extern "C" fn Download( - hwnd_parent: HWND, - string_size: u32, - variables: *mut wchar_t, - stacktop: *mut *mut stack_t, -) { - exdll_init(string_size, variables, stacktop); - - let url = popstring().unwrap(); - let path = popstring().unwrap(); - - let status = download_file(hwnd_parent, &url, &path); - pushint(status); -} - -fn download_file(hwnd_parent: HWND, url: &str, path: &str) -> i32 { - let childhwnd; - let mut progress_bar: HWND = 0; - let mut progress_text: HWND = 0; - let mut downloading_text: HWND = 0; - let mut details_section: HWND = 0; - let mut details_section_resized = false; - let mut details_section_resized_back = false; - - if hwnd_parent != 0 { - childhwnd = find_window(hwnd_parent, "#32770"); - if childhwnd != 0 { - details_section = find_window(childhwnd, "SysListView32"); - let expanded = is_visible(details_section); - unsafe { - progress_bar = CreateWindowExW( - 0, - PROGRESS_CLASSW, - std::ptr::null(), - WS_CHILD | WS_VISIBLE, - 0, - if expanded { 40 } else { 75 }, - 450, - 18, - childhwnd, - 0, - 0, - std::ptr::null(), - ); - - downloading_text = CreateWindowExW( - 0, - WC_STATICW, - std::ptr::null(), - WS_CHILD | WS_VISIBLE, - 0, - if expanded { 60 } else { 95 }, - 450, - 18, - childhwnd, - 0, - 0, - std::ptr::null(), - ); - - progress_text = CreateWindowExW( - 0, - WC_STATICW, - std::ptr::null(), - WS_CHILD | WS_VISIBLE, - 0, - if expanded { 78 } else { 113 }, - 450, - 18, - childhwnd, - 0, - 0, - std::ptr::null(), - ); - - let font = SendMessageW(childhwnd, WM_GETFONT, 0, 0); - SendMessageW(downloading_text, WM_SETFONT, font as _, 0); - SendMessageW(progress_text, WM_SETFONT, font as _, 0); - }; - } - } - - let response = match ureq::get(url).call() { - Ok(data) => data, - Err(err) => { - return match err { - ureq::Error::Status(code, _) => code as i32, - ureq::Error::Transport(_) => 499, - } - } - }; - - let total = response - .header("Content-Length") - .unwrap_or("0") - .parse::() - .unwrap(); - - let mut read = 0; - - let mut reader = response.into_reader(); - let mut reader = ProgressReader::new(&mut reader, |progress: usize| { - let expanded = is_visible(details_section); - if expanded && !details_section_resized { - unsafe { - SetWindowPos(progress_bar, 0, 0, 40, 0, 0, SWP_NOSIZE); - SetWindowPos(downloading_text, 0, 0, 60, 0, 0, SWP_NOSIZE); - SetWindowPos(progress_text, 0, 0, 78, 0, 0, SWP_NOSIZE); - - SetWindowPos(details_section, 0, 0, 100, 450, 120, SWP_FRAMECHANGED); - } - details_section_resized = true; - } - - read += progress; - - let percentage = (read as f64 / total as f64) * 100.0; - unsafe { SendMessageW(progress_bar, PBM_SETPOS, percentage as _, 0) }; - - let text = pluginapi::encode_wide(format!( - "{} / {} KiB - {:.2}%", - read / 1024, - total / 1024, - percentage, - )); - unsafe { SetWindowTextW(progress_text, text.as_ptr()) }; - - let text = pluginapi::encode_wide(format!("Downloading {} ...", url)); - unsafe { SetWindowTextW(downloading_text, text.as_ptr()) }; - - if percentage >= 100. && !details_section_resized_back { - unsafe { - ShowWindow(progress_bar, SW_HIDE); - ShowWindow(progress_text, SW_HIDE); - ShowWindow(downloading_text, SW_HIDE); - SetWindowPos(details_section, 0, 0, 41, 450, 180, SWP_FRAMECHANGED); - } - details_section_resized_back = true; - } - }); - - let path = Path::new(path); - fs::create_dir_all(path.parent().unwrap_or_else(|| Path::new("."))).unwrap(); - - let mut file = fs::File::options() - .create(true) - .write(true) - .truncate(true) - .open(path) - .unwrap(); - - let res = io::copy(&mut reader, &mut file); - - i32::from(res.is_err()) -} - -fn find_window(parent: HWND, class: impl AsRef) -> HWND { - let class = pluginapi::encode_wide(class.as_ref()); - unsafe { FindWindowExW(parent, 0, class.as_ptr(), std::ptr::null()) } -} - -fn is_visible(hwnd: HWND) -> bool { - let style = unsafe { GetWindowLongPtrW(hwnd, GWL_STYLE) }; - (style & !WS_VISIBLE as i32) != style -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_downloads() { - assert_eq!( - download_file( - 0, - "https://go.microsoft.com/fwlink/p/?LinkId=2124703", - "wv2setup.exe" - ), - 0 - ) - } -} diff --git a/crates/nsis-fn/Cargo.toml b/crates/nsis-fn/Cargo.toml new file mode 100644 index 0000000..bb7727f --- /dev/null +++ b/crates/nsis-fn/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "nsis-fn" +version = "0.0.0" +edition = "2021" +license = "MIT OR Apache-2.0" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1" +quote = "1" +syn = { version = "2", features = ["full"] } diff --git a/crates/nsis-fn/src/lib.rs b/crates/nsis-fn/src/lib.rs new file mode 100644 index 0000000..885fb27 --- /dev/null +++ b/crates/nsis-fn/src/lib.rs @@ -0,0 +1,51 @@ +use proc_macro::TokenStream; +use proc_macro2::Span; +use quote::quote; +use syn::{parse::Parse, parse_macro_input, Ident, ItemFn}; + +struct NsisFn { + func: ItemFn, +} + +impl Parse for NsisFn { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let func: ItemFn = input.parse()?; + Ok(Self { func }) + } +} + +/// Generates a wrapper NSIS compliant dll export that calls `nsis_plugin_api::exdll_init` +/// automatically. This macro expects the function to return a `Result<(), nsis_plugin_api::Error>` +/// and will automatically push the error to NSIS stack on failure. +#[proc_macro_attribute] +pub fn nsis_fn(_attr: TokenStream, tokens: TokenStream) -> TokenStream { + let tokens = parse_macro_input!(tokens as NsisFn); + let NsisFn { func } = tokens; + + let ident = func.sig.ident; + let block = func.block; + let attrs = func.attrs; + + let new_ident = Ident::new(&format!("__{}", ident), Span::call_site()); + + quote! { + #[inline(always)] + pub unsafe fn #new_ident() -> Result<(), ::nsis_plugin_api::Error> #block + + #(#attrs)* + #[no_mangle] + #[allow(non_standard_style)] + pub unsafe extern "C" fn #ident( + hwnd_parent: ::windows_sys::Win32::Foundation::HWND, + string_size: core::ffi::c_int, + variables: *mut ::nsis_plugin_api::wchar_t, + stacktop: *mut *mut ::nsis_plugin_api::stack_t, + ) { + ::nsis_plugin_api::exdll_init(string_size, variables, stacktop); + if let Err(e) = #new_ident() { + e.push_err(); + } + } + } + .into() +} diff --git a/crates/pluginapi/Cargo.toml b/crates/nsis-plugin-api/Cargo.toml similarity index 68% rename from crates/pluginapi/Cargo.toml rename to crates/nsis-plugin-api/Cargo.toml index 0e006cf..89a1cbb 100644 --- a/crates/pluginapi/Cargo.toml +++ b/crates/nsis-plugin-api/Cargo.toml @@ -1,8 +1,9 @@ [package] -name = "pluginapi" +name = "nsis-plugin-api" version = "0.0.0" edition = "2021" license = "MIT OR Apache-2.0" [dependencies] windows-sys = { workspace = true } +nsis-fn = { path = "../nsis-fn" } diff --git a/crates/nsis-plugin-api/src/lib.rs b/crates/nsis-plugin-api/src/lib.rs new file mode 100644 index 0000000..3f70612 --- /dev/null +++ b/crates/nsis-plugin-api/src/lib.rs @@ -0,0 +1,254 @@ +#![no_std] +#![allow(unused)] +#![allow(nonstandard_style)] + +extern crate alloc; + +use core::{ + ffi::{c_int, c_void}, + fmt::Display, + iter, + mem::{size_of, size_of_val}, +}; + +use alloc::string::{String, ToString}; +use alloc::vec; +use alloc::{ + alloc::{GlobalAlloc, Layout}, + vec::Vec, +}; + +use windows_sys::Win32::{ + Foundation::GlobalFree, + Globalization::{lstrcpyW, lstrcpynW}, + System::Memory::{ + GetProcessHeap, GlobalAlloc, HeapAlloc, HeapFree, HeapReAlloc, GPTR, HEAP_ZERO_MEMORY, + }, +}; + +pub use nsis_fn::nsis_fn; + +pub type wchar_t = i32; + +#[repr(C)] +#[derive(Debug)] +pub struct stack_t { + pub next: *mut stack_t, + pub text: [wchar_t; 1], +} + +pub static mut G_STRINGSIZE: c_int = 0; +pub static mut G_VARIABLES: *mut wchar_t = core::ptr::null_mut(); +pub static mut G_STACKTOP: *mut *mut stack_t = core::ptr::null_mut(); + +/// Initis the global variables used by NSIS functions: [`push`], [`pushstr`], [`pushint`], [`pop`], [`popstr`] and [`popint`] +/// +/// # Safety +/// +/// This function mutates static variables and should only be called in a function +#[inline(always)] +pub unsafe fn exdll_init(string_size: c_int, variables: *mut wchar_t, stacktop: *mut *mut stack_t) { + G_STRINGSIZE = string_size; + G_VARIABLES = variables; + G_STACKTOP = stacktop; +} + +pub const ONE: &[u16; 2] = &[49, 0]; +pub const ZERO: &[u16; 2] = &[48, 0]; +pub const NEGATIVE_ONE: &[u16; 3] = &[45, 49, 0]; + +#[derive(Debug)] +pub enum Error { + StackIsNull, + ParseIntError, +} + +impl Error { + const fn description(&self) -> &str { + match self { + Error::StackIsNull => "Stack is null", + Error::ParseIntError => "Failed to parse integer", + } + } + pub fn push_err(&self) { + let _ = unsafe { pushstr(self.description()) }; + } +} + +/// Pushes some bytes onto the NSIS stack. +/// +/// # Safety +/// +/// This function reads static variables and should only be called after [`exdll_init`] is called. +pub unsafe fn push(bytes: &[u16]) -> Result<(), Error> { + if G_STACKTOP.is_null() { + return Err(Error::StackIsNull); + } + + let n = size_of::() + G_STRINGSIZE as usize * 2; + let th = GlobalAlloc(GPTR, n) as *mut stack_t; + lstrcpyW((*th).text.as_ptr() as _, bytes.as_ptr()); + (*th).next = *G_STACKTOP; + *G_STACKTOP = th; + + Ok(()) +} + +/// Pushes a string onto the NSIS stack. +/// +/// # Safety +/// +/// This function reads static variables and should only be called after [`exdll_init`] is called. +pub unsafe fn pushstr(str: &str) -> Result<(), Error> { + let bytes = encode_utf16(str); + push(&bytes) +} + +/// Pushes an integer onto the NSIS stack. +/// +/// # Safety +/// +/// This function reads static variables and should only be called after [`exdll_init`] is called. +pub unsafe fn pushint(int: i32) -> Result<(), Error> { + let str = int.to_string(); + pushstr(&str) +} + +/// Pops bytes from NSIS stack. +/// +/// # Safety +/// +/// This function reads static variables and should only be called after [`exdll_init`] is called. +pub unsafe fn pop() -> Result, Error> { + if G_STACKTOP.is_null() || (*G_STACKTOP).is_null() { + return Err(Error::StackIsNull); + } + + let mut out = vec![0_u16; G_STRINGSIZE as _]; + + let th: *mut stack_t = *G_STACKTOP; + lstrcpyW(out.as_mut_ptr(), (*th).text.as_ptr() as _); + *G_STACKTOP = (*th).next; + GlobalFree(th as _); + + Ok(out) +} + +/// Pops a string from NSIS stack. +/// +/// # Safety +/// +/// This function reads static variables and should only be called after [`exdll_init`] is called. +pub unsafe fn popstr() -> Result { + let bytes = pop()?; + Ok(decode_utf16_lossy(&bytes)) +} + +/// Pops an integer from NSIS stack. +/// +/// # Safety +/// +/// This function reads static variables and should only be called after [`exdll_init`] is called. +pub unsafe fn popint() -> Result { + let str = popstr()?; + str.parse().map_err(|_| Error::ParseIntError) +} + +pub fn encode_utf16(str: &str) -> Vec { + str.encode_utf16() + .chain(iter::once(0)) + .collect::>() +} + +pub fn decode_utf16_lossy(bytes: &[u16]) -> String { + let bytes = bytes + .iter() + .position(|c| *c == 0) + .map(|nul| &bytes[..nul]) + .unwrap_or(bytes); + String::from_utf16_lossy(bytes) +} + +#[global_allocator] +static WIN32_ALLOCATOR: Heapalloc = Heapalloc; + +pub struct Heapalloc; + +unsafe impl GlobalAlloc for Heapalloc { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + HeapAlloc(GetProcessHeap(), 0, layout.size()) as *mut u8 + } + + unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { + HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, layout.size()) as *mut u8 + } + + unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) { + HeapFree(GetProcessHeap(), 0, ptr as *mut c_void); + } + + unsafe fn realloc(&self, ptr: *mut u8, _layout: Layout, new_size: usize) -> *mut u8 { + HeapReAlloc( + GetProcessHeap(), + HEAP_ZERO_MEMORY, + ptr as *mut c_void, + new_size, + ) as *mut u8 + } +} + +/// Sets up the needed functions for the NSIS plugin dll, +/// like `main`, `panic` and `mem*` extern functions +#[macro_export] +macro_rules! nsis_plugin { + () => { + #[no_mangle] + extern "system" fn DllMain( + dll_module: ::windows_sys::Win32::Foundation::HINSTANCE, + call_reason: u32, + _: *mut (), + ) -> bool { + true + } + + #[cfg(not(test))] + #[panic_handler] + fn panic(_info: &core::panic::PanicInfo) -> ! { + unsafe { ::windows_sys::Win32::System::Threading::ExitProcess(u32::MAX) } + } + + #[no_mangle] + pub unsafe extern "C" fn memcpy(dest: *mut u8, src: *const u8, n: isize) -> *mut u8 { + let mut i = 0; + while i < n { + *dest.offset(i) = *src.offset(i); + i += 1; + } + return dest; + } + + #[no_mangle] + pub unsafe extern "C" fn memcmp(s1: *const u8, s2: *const u8, n: isize) -> i32 { + let mut i = 0; + while i < n { + let a = *s1.offset(i); + let b = *s2.offset(i); + if a != b { + return a as i32 - b as i32; + } + i += 1; + } + return 0; + } + + #[no_mangle] + pub unsafe extern "C" fn memset(s: *mut u8, c: i32, n: isize) -> *mut u8 { + let mut i = 0; + while i < n { + *s.offset(i) = c as u8; + i += 1; + } + return s; + } + }; +} diff --git a/crates/nsis-process/Cargo.toml b/crates/nsis-process/Cargo.toml index 0867d69..a0cd6fa 100644 --- a/crates/nsis-process/Cargo.toml +++ b/crates/nsis-process/Cargo.toml @@ -6,8 +6,11 @@ edition = { workspace = true } license = { workspace = true } [lib] -crate-type = [ "rlib", "cdylib" ] +crate-type = ["cdylib"] + +[features] +test = [] [dependencies] -pluginapi = { workspace = true } +nsis-plugin-api = { workspace = true } windows-sys = { workspace = true } diff --git a/crates/nsis-process/build.rs b/crates/nsis-process/build.rs new file mode 100644 index 0000000..ce8d102 --- /dev/null +++ b/crates/nsis-process/build.rs @@ -0,0 +1,5 @@ +fn main() { + if std::env::var("CARGO_FEATURE_TEST").as_deref() != Ok("1") { + println!("cargo::rustc-link-arg=/ENTRY:DllMain") + } +} diff --git a/crates/nsis-process/src/lib.rs b/crates/nsis-process/src/lib.rs index e855f8d..3fd6250 100644 --- a/crates/nsis-process/src/lib.rs +++ b/crates/nsis-process/src/lib.rs @@ -1,9 +1,14 @@ -use std::{ffi::c_void, mem, ptr}; +#![no_std] -use pluginapi::{decode_wide, exdll_init, popstring, pushint, stack_t, wchar_t}; +extern crate alloc; +use alloc::vec; +use alloc::vec::Vec; +use core::{ffi::c_void, mem, ptr}; + +use nsis_plugin_api::*; use windows_sys::Win32::{ - Foundation::{CloseHandle, HANDLE, HWND}, + Foundation::{CloseHandle, HANDLE}, Security::{EqualSid, GetTokenInformation, TokenUser, TOKEN_QUERY, TOKEN_USER}, System::{ Diagnostics::ToolHelp::{ @@ -11,32 +16,27 @@ use windows_sys::Win32::{ TH32CS_SNAPPROCESS, }, Threading::{ - OpenProcess, OpenProcessToken, TerminateProcess, PROCESS_QUERY_INFORMATION, - PROCESS_TERMINATE, + GetCurrentProcessId, OpenProcess, OpenProcessToken, TerminateProcess, + PROCESS_QUERY_INFORMATION, PROCESS_TERMINATE, }, }, }; +nsis_plugin!(); + /// Test if there is a running process with the given name, skipping processes with the host's pid. The input and process names are case-insensitive. /// /// # Safety /// /// This function always expects 1 string on the stack ($1: name) and will panic otherwise. -#[no_mangle] -pub unsafe extern "C" fn FindProcess( - _hwnd_parent: HWND, - string_size: u32, - variables: *mut wchar_t, - stacktop: *mut *mut stack_t, -) { - exdll_init(string_size, variables, stacktop); - - let name = popstring().unwrap(); +#[nsis_fn] +fn FindProcess() -> Result<(), Error> { + let name = popstr()?; if !get_processes(&name).is_empty() { - pushint(0); + push(ZERO) } else { - pushint(1); + push(ONE) } } @@ -45,33 +45,26 @@ pub unsafe extern "C" fn FindProcess( /// # Safety /// /// This function always expects 1 string on the stack ($1: name) and will panic otherwise. -#[no_mangle] -pub unsafe extern "C" fn FindProcessCurrentUser( - _hwnd_parent: HWND, - string_size: u32, - variables: *mut wchar_t, - stacktop: *mut *mut stack_t, -) { - exdll_init(string_size, variables, stacktop); - - let name = popstring().unwrap(); +#[nsis_fn] +fn FindProcessCurrentUser() -> Result<(), Error> { + let name = popstr()?; let processes = get_processes(&name); - if let Some(user_sid) = get_sid(std::process::id()) { + if let Some(user_sid) = get_sid(GetCurrentProcessId()) { if processes .into_iter() .any(|pid| belongs_to_user(user_sid, pid)) { - pushint(0); + push(ZERO) } else { - pushint(1); + push(ONE) } // Fall back to perMachine checks if we can't get current user id } else if processes.is_empty() { - pushint(1); + push(ONE) } else { - pushint(0); + push(ZERO) } } @@ -80,23 +73,16 @@ pub unsafe extern "C" fn FindProcessCurrentUser( /// # Safety /// /// This function always expects 1 string on the stack ($1: name) and will panic otherwise. -#[no_mangle] -pub unsafe extern "C" fn KillProcess( - _hwnd_parent: HWND, - string_size: u32, - variables: *mut wchar_t, - stacktop: *mut *mut stack_t, -) { - exdll_init(string_size, variables, stacktop); - - let name = popstring().unwrap(); +#[nsis_fn] +fn KillProcess() -> Result<(), Error> { + let name = popstr()?; let processes = get_processes(&name); if !processes.is_empty() && processes.into_iter().map(kill).all(|b| b) { - pushint(0); + push(ZERO) } else { - pushint(1); + push(ONE) } } @@ -105,25 +91,17 @@ pub unsafe extern "C" fn KillProcess( /// # Safety /// /// This function always expects 1 string on the stack ($1: name) and will panic otherwise. -#[no_mangle] -pub unsafe extern "C" fn KillProcessCurrentUser( - _hwnd_parent: HWND, - string_size: u32, - variables: *mut wchar_t, - stacktop: *mut *mut stack_t, -) { - exdll_init(string_size, variables, stacktop); - - let name = popstring().unwrap(); +#[nsis_fn] +fn KillProcessCurrentUser() -> Result<(), Error> { + let name = popstr()?; let processes = get_processes(&name); if processes.is_empty() { - pushint(1); - return; + return push(ONE); } - let success = if let Some(user_sid) = get_sid(std::process::id()) { + let success = if let Some(user_sid) = get_sid(GetCurrentProcessId()) { processes .into_iter() .filter(|pid| belongs_to_user(user_sid, *pid)) @@ -134,9 +112,9 @@ pub unsafe extern "C" fn KillProcessCurrentUser( }; if success { - pushint(0) + push(ZERO) } else { - pushint(1) + push(ONE) } } @@ -204,7 +182,7 @@ unsafe fn get_sid(pid: u32) -> Option<*mut c_void> { } fn get_processes(name: &str) -> Vec { - let current_pid = std::process::id(); + let current_pid = unsafe { GetCurrentProcessId() }; let mut processes = Vec::new(); unsafe { @@ -218,11 +196,7 @@ fn get_processes(name: &str) -> Vec { if Process32FirstW(handle, &mut process) != 0 { while Process32NextW(handle, &mut process) != 0 { if current_pid != process.th32ProcessID - && decode_wide(&process.szExeFile) - .to_str() - .unwrap_or_default() - .to_lowercase() - == name.to_lowercase() + && decode_utf16_lossy(&process.szExeFile).to_lowercase() == name.to_lowercase() { processes.push(process.th32ProcessID); } @@ -242,14 +216,12 @@ mod tests { #[test] fn find_process() { let processes = get_processes("explorer.exe"); - dbg!(&processes); assert!(!processes.is_empty()); } #[test] fn kill_process() { let processes = get_processes("something_that_doesnt_exist.exe"); - dbg!(&processes); // TODO: maybe find some way to spawn a dummy process we can kill here? // This will return true on empty iterators so it's basically no-op right now assert!(processes.into_iter().map(kill).all(|b| b)); diff --git a/crates/nsis-semvercompare/Cargo.toml b/crates/nsis-semvercompare/Cargo.toml index 0b8ccb2..15e9f95 100644 --- a/crates/nsis-semvercompare/Cargo.toml +++ b/crates/nsis-semvercompare/Cargo.toml @@ -6,9 +6,12 @@ edition = { workspace = true } license = { workspace = true } [lib] -crate-type = ["rlib", "cdylib"] +crate-type = ["cdylib"] + +[features] +test = [] [dependencies] -semver = "1.0" -pluginapi = { workspace = true } +semver = { version = "1.0", default-features = false } +nsis-plugin-api = { workspace = true } windows-sys = { workspace = true } diff --git a/crates/nsis-semvercompare/build.rs b/crates/nsis-semvercompare/build.rs new file mode 100644 index 0000000..ce8d102 --- /dev/null +++ b/crates/nsis-semvercompare/build.rs @@ -0,0 +1,5 @@ +fn main() { + if std::env::var("CARGO_FEATURE_TEST").as_deref() != Ok("1") { + println!("cargo::rustc-link-arg=/ENTRY:DllMain") + } +} diff --git a/crates/nsis-semvercompare/src/lib.rs b/crates/nsis-semvercompare/src/lib.rs index c8b49d4..d8bca47 100644 --- a/crates/nsis-semvercompare/src/lib.rs +++ b/crates/nsis-semvercompare/src/lib.rs @@ -1,33 +1,37 @@ -use std::{cmp::Ordering, str::FromStr}; +#![no_std] -use pluginapi::{exdll_init, popstring, pushint, stack_t, wchar_t}; +use core::cmp::Ordering; + +use nsis_plugin_api::*; use semver::Version; -use windows_sys::Win32::Foundation::HWND; + +nsis_plugin!(); /// Compare two semantic versions. /// +/// Returns `0` if equal, `1` if `$v1` is newer and `-1` if `$v2` is newer. +/// /// # Safety /// -/// This function always expects 2 strings on the stack ($1: version1, $2: version2) and will panic otherwise. -#[no_mangle] -pub unsafe extern "C" fn SemverCompare( - _hwnd_parent: HWND, - string_size: u32, - variables: *mut wchar_t, - stacktop: *mut *mut stack_t, -) { - exdll_init(string_size, variables, stacktop); +/// This function always expects 2 strings on the stack ($v1, $v2) and will panic otherwise. +#[nsis_fn] +fn SemverCompare() -> Result<(), Error> { + let v1 = popstr()?; + let v2 = popstr()?; - let v1 = popstring().unwrap(); - let v2 = popstring().unwrap(); + match compare(&v1, &v2) { + -1 => push(NEGATIVE_ONE)?, + 0 => push(ZERO)?, + 1 => push(ONE)?, + _ => unreachable!(), + } - let ret = semver_compare(&v1, &v2); - pushint(ret); + Ok(()) } -fn semver_compare(v1: &str, v2: &str) -> i32 { - let v1 = Version::from_str(v1); - let v2 = Version::from_str(v2); +fn compare(v1: &str, v2: &str) -> i32 { + let v1 = Version::parse(v1); + let v2 = Version::parse(v2); let (v1, v2) = match (v1, v2) { (Ok(_), Err(_)) => return 1, @@ -64,7 +68,7 @@ mod tests { ("1.2.1-fffasd.1", "1.2.1-dasdqwe.1", 1), ("1.2.1-gasfdlkj.1", "1.2.1-calskjd.1", 1), ] { - assert_eq!(semver_compare(v1, v2), ret); + assert_eq!(compare(v1, v2), ret); } } } diff --git a/crates/nsis-tauri-utils/Cargo.toml b/crates/nsis-tauri-utils/Cargo.toml index 393dd67..40918a4 100644 --- a/crates/nsis-tauri-utils/Cargo.toml +++ b/crates/nsis-tauri-utils/Cargo.toml @@ -1,14 +1,17 @@ [package] name = "nsis-tauri-utils" -version = "0.2.2" +version = "0.2.0" authors = { workspace = true } edition = { workspace = true } license = { workspace = true } [lib] -crate-type = [ "cdylib" ] +crate-type = ["cdylib"] + +[features] +test = [] [dependencies] -nsis-download = { path = "../nsis-download" } -nsis-process = { path = "../nsis-process" } -nsis-semvercompare = { path = "../nsis-semvercompare" } +nsis-plugin-api = { workspace = true } +windows-sys = { workspace = true } +semver = { version = "1.0", default-features = false } diff --git a/crates/nsis-tauri-utils/build.rs b/crates/nsis-tauri-utils/build.rs new file mode 100644 index 0000000..3a7a7cf --- /dev/null +++ b/crates/nsis-tauri-utils/build.rs @@ -0,0 +1,44 @@ +fn main() { + combine_plugins_and_write_to_out_dir(); + if std::env::var("CARGO_FEATURE_TEST").as_deref() != Ok("1") { + println!("cargo::rustc-link-arg=/ENTRY:DllMain") + } +} + +/// Combines the plugins into one file that is included in lib.rs +/// using `include!(concat!(env!("OUT_DIR"), "/combined_libs.rs"));` +/// +/// Plugins are combined this way because it saves a few kilobytes in the generated DLL +/// than the making nsis-tauri-utils depend on other plugins and re-export the DLLs +fn combine_plugins_and_write_to_out_dir() { + let out_dir = std::env::var("OUT_DIR").unwrap(); + let path = format!("{out_dir}/combined_libs.rs"); + + let mut file = std::fs::File::options() + .truncate(true) + .write(true) + .create(true) + .open(path) + .unwrap(); + + for plugin in [ + include_str!("../nsis-semvercompare/src/lib.rs"), + include_str!("../nsis-process/src/lib.rs"), + ] { + let lines = plugin + .lines() + .filter(|l| { + // remove lines that should only be specified once + // either for compilation or for clippy + !(l.contains("#![no_std]") + || l.contains("nsis_plugin!();") + || l.contains("use nsis_plugin_api::*;")) + }) + .take_while(|l| !l.contains("mod tests {")) + .collect::>(); + + // skip last line which should be #[cfg(test)] + let content = lines[..lines.len() - 1].join("\n"); + std::io::Write::write_all(&mut file, content.as_bytes()).unwrap(); + } +} diff --git a/crates/nsis-tauri-utils/src/lib.rs b/crates/nsis-tauri-utils/src/lib.rs index 95d4ac3..155bf34 100644 --- a/crates/nsis-tauri-utils/src/lib.rs +++ b/crates/nsis-tauri-utils/src/lib.rs @@ -1,3 +1,7 @@ -pub use nsis_download::*; -pub use nsis_process::*; -pub use nsis_semvercompare::*; +#![no_std] + +use nsis_plugin_api::*; + +nsis_plugin!(); + +include!(concat!(env!("OUT_DIR"), "/combined_libs.rs")); diff --git a/crates/pluginapi/src/lib.rs b/crates/pluginapi/src/lib.rs deleted file mode 100644 index 9cb27b9..0000000 --- a/crates/pluginapi/src/lib.rs +++ /dev/null @@ -1,93 +0,0 @@ -#![allow(clippy::missing_safety_doc)] -#![allow(non_camel_case_types)] - -use std::{ - ffi::{OsStr, OsString}, - iter::once, - mem::{size_of, size_of_val}, - os::windows::prelude::{OsStrExt, OsStringExt}, -}; - -use windows_sys::Win32::{ - Foundation::GlobalFree, - Globalization::{lstrcpyW, lstrcpynW}, - System::Memory::{GlobalAlloc, GPTR}, -}; - -static mut G_STRINGSIZE: u32 = 0; -static mut G_VARIABLES: *mut wchar_t = std::ptr::null_mut(); -static mut G_STACKTOP: *mut *mut stack_t = std::ptr::null_mut(); - -pub unsafe fn exdll_init(string_size: u32, variables: *mut wchar_t, stacktop: *mut *mut stack_t) { - G_STRINGSIZE = string_size; - G_VARIABLES = variables; - G_STACKTOP = stacktop; -} - -pub type wchar_t = i32; - -#[derive(Debug)] -pub enum Error { - InvalidStackError, - InvalidUnicode, -} - -#[repr(C)] -#[derive(Debug)] -pub struct stack_t { - next: *mut stack_t, - text: [wchar_t; 1], -} - -pub unsafe fn pushstring(s: impl AsRef) { - if G_STACKTOP.is_null() { - return; - } - - let string_wide = encode_wide(s); - let th: *mut stack_t = GlobalAlloc( - GPTR, - size_of::() + G_STRINGSIZE as usize * size_of_val(&string_wide), - ) as _; - lstrcpynW( - (*th).text.as_ptr() as _, - string_wide.as_ptr() as _, - G_STRINGSIZE as _, - ); - (*th).next = *G_STACKTOP; - *G_STACKTOP = th; -} - -pub unsafe fn popstring() -> Result { - if G_STACKTOP.is_null() || (*G_STACKTOP).is_null() { - return Err(Error::InvalidStackError); - } - - let mut string_wide: Vec = vec![0; G_STRINGSIZE as _]; - let th: *mut stack_t = *G_STACKTOP; - lstrcpyW(string_wide.as_mut_ptr(), (*th).text.as_ptr() as _); - let string = decode_wide(&string_wide) - .to_str() - .ok_or(Error::InvalidUnicode)? - .to_string(); - *G_STACKTOP = (*th).next; - GlobalFree(th as _); - - Ok(string) -} - -pub unsafe fn pushint(int: i32) { - pushstring(int.to_string()) -} - -pub fn encode_wide(string: impl AsRef) -> Vec { - string.as_ref().encode_wide().chain(once(0)).collect() -} - -pub fn decode_wide(mut wide_c_string: &[u16]) -> OsString { - if let Some(null_pos) = wide_c_string.iter().position(|c| *c == 0) { - wide_c_string = &wide_c_string[..null_pos]; - } - - OsString::from_wide(wide_c_string) -} diff --git a/demo.nsi b/demo.nsi index 52d164f..b1f9c94 100644 --- a/demo.nsi +++ b/demo.nsi @@ -1,19 +1,11 @@ Name "demo" OutFile "demo.exe" Unicode true +ShowInstDetails show -!addplugindir ".\target\release" -!addplugindir ".\target\debug" !addplugindir ".\target\i686-pc-windows-msvc\release" -!addplugindir ".\target\i686-pc-windows-msvc\debug" -!addplugindir "$%CARGO_TARGET_DIR%\release" -!addplugindir "$%CARGO_TARGET_DIR%\debug" !addplugindir "$%CARGO_TARGET_DIR%\i686-pc-windows-msvc\release" -!addplugindir "$%CARGO_TARGET_DIR%\i686-pc-windows-msvc\debug" -!addplugindir "$%CARGO_BUILD_TARGET_DIR%\release" -!addplugindir "$%CARGO_BUILD_TARGET_DIR%\debug" !addplugindir "$%CARGO_BUILD_TARGET_DIR%\i686-pc-windows-msvc\release" -!addplugindir "$%CARGO_BUILD_TARGET_DIR%\i686-pc-windows-msvc\debug" !include "MUI2.nsh" @@ -23,14 +15,11 @@ Unicode true Section nsis_semvercompare::SemverCompare "1.0.0" "1.1.0" Pop $1 - DetailPrint $1 + DetailPrint "SemverCompare(1.0.0, 1.1.0): $1" nsis_process::FindProcess "explorer.exe" Pop $1 - DetailPrint $1 + DetailPrint "FindProcess(explorer.exe): $1" nsis_process::FindProcess "abcdef.exe" Pop $1 - DetailPrint $1 - nsis_download::Download "https://go.microsoft.com/fwlink/p/?LinkId=2124703" "wv2setup.exe" - Pop $1 - DetailPrint $1 + DetailPrint "FindProcess(abcdef.exe): $1" SectionEnd \ No newline at end of file