From 661cc022f8d752847cd2dd423aee926572b84bf6 Mon Sep 17 00:00:00 2001 From: Caelan Sayler Date: Sun, 25 May 2025 01:06:25 +0100 Subject: [PATCH] Refactor string handling to use OsString where possible --- src/bins/src/commands/apply.rs | 16 +- src/bins/src/commands/apply_linux_impl.rs | 2 +- src/bins/src/commands/apply_windows_impl.rs | 9 +- src/bins/src/commands/install.rs | 48 ++--- src/bins/src/commands/patch.rs | 14 +- src/bins/src/commands/start.rs | 16 +- src/bins/src/commands/start_windows_impl.rs | 52 +++--- src/bins/src/commands/uninstall.rs | 2 +- src/bins/src/setup.rs | 3 +- src/bins/src/shared/dialogs_windows.rs | 8 +- src/bins/src/shared/util_osx.rs | 2 +- src/bins/src/shared/util_windows.rs | 20 +-- src/bins/src/stub.rs | 2 +- src/bins/src/update.rs | 55 +++--- src/bins/src/windows/known_path.rs | 43 +++-- src/bins/src/windows/mod.rs | 1 - src/bins/src/windows/prerequisite.rs | 19 +- src/bins/src/windows/registry.rs | 20 ++- src/bins/src/windows/runtimes.rs | 16 +- src/bins/src/windows/shortcuts.rs | 144 +++++++-------- src/bins/src/windows/splash.rs | 2 +- src/bins/src/windows/strings.rs | 149 --------------- src/bins/src/windows/util.rs | 73 ++++---- src/lib-cpp/src/csource.rs | 12 +- src/lib-cpp/src/types.rs | 2 +- src/lib-rust/src/bundle.rs | 78 ++++---- src/lib-rust/src/download.rs | 19 +- src/lib-rust/src/lib.rs | 23 +-- src/lib-rust/src/locator.rs | 51 +----- src/lib-rust/src/manager.rs | 95 +++++----- src/lib-rust/src/process_win.rs | 77 ++++---- src/lib-rust/src/sources.rs | 29 +-- src/lib-rust/src/wide_strings.rs | 190 ++++++++++++++++++++ 33 files changed, 642 insertions(+), 650 deletions(-) delete mode 100644 src/bins/src/windows/strings.rs create mode 100644 src/lib-rust/src/wide_strings.rs diff --git a/src/bins/src/commands/apply.rs b/src/bins/src/commands/apply.rs index e554a20d..cc69fc4f 100644 --- a/src/bins/src/commands/apply.rs +++ b/src/bins/src/commands/apply.rs @@ -1,7 +1,7 @@ use crate::shared::{self, OperationWait}; -use velopack::{locator, locator::VelopackLocator, constants}; use anyhow::{bail, Result}; -use std::path::PathBuf; +use std::{ffi::OsString, path::PathBuf}; +use velopack::{constants, locator, locator::VelopackLocator}; #[cfg(target_os = "linux")] use super::apply_linux_impl::apply_package_impl; @@ -15,7 +15,7 @@ pub fn apply<'a>( restart: bool, wait: OperationWait, package: Option<&PathBuf>, - exe_args: Option>, + exe_args: Option>, run_hooks: bool, ) -> Result { shared::operation_wait(wait); @@ -25,10 +25,12 @@ pub fn apply<'a>( match package { Some(package) => { - info!("Getting ready to apply package to {} ver {}: {}", - locator.get_manifest_id(), - locator.get_manifest_version_full_string(), - package.to_string_lossy()); + info!( + "Getting ready to apply package to {} ver {}: {:?}", + locator.get_manifest_id(), + locator.get_manifest_version_full_string(), + package + ); match apply_package_impl(&locator, &package, run_hooks) { Ok(applied_locator) => { info!("Package version {} applied successfully.", applied_locator.get_manifest_version_full_string()); diff --git a/src/bins/src/commands/apply_linux_impl.rs b/src/bins/src/commands/apply_linux_impl.rs index 1cb6f4e0..b8b03d6e 100644 --- a/src/bins/src/commands/apply_linux_impl.rs +++ b/src/bins/src/commands/apply_linux_impl.rs @@ -6,7 +6,7 @@ use velopack::{bundle, locator::VelopackLocator}; pub fn apply_package_impl<'a>(locator: &VelopackLocator, pkg: &PathBuf, _runhooks: bool) -> Result { // on linux, the current "dir" is actually an AppImage file which we need to replace. - info!("Loading bundle from {}", pkg.to_string_lossy()); + info!("Loading bundle from {:?}", pkg); let mut bundle = bundle::load_bundle_from_file(pkg)?; let manifest = bundle.read_manifest()?; let temp_path = locator.get_temp_dir_rand16().to_string_lossy().to_string(); diff --git a/src/bins/src/commands/apply_windows_impl.rs b/src/bins/src/commands/apply_windows_impl.rs index 3f77433e..550decf5 100644 --- a/src/bins/src/commands/apply_windows_impl.rs +++ b/src/bins/src/commands/apply_windows_impl.rs @@ -4,7 +4,7 @@ use crate::{ windows::{self, splash}, }; use anyhow::{bail, Context, Result}; -use std::{fs, path::PathBuf}; +use std::{ffi::OsString, fs, path::PathBuf}; use std::{sync::mpsc, time::Duration}; use velopack::{bundle::load_bundle_from_file, constants, locator::VelopackLocator, process}; @@ -51,8 +51,7 @@ pub fn apply_package_impl(old_locator: &VelopackLocator, package: &PathBuf, run_ } else { info!("Re-launching as administrator to update in {:?}", root_path); - let package_string = package.to_string_lossy().to_string(); - let args = vec!["--norestart".to_string(), "--package".to_string(), package_string]; + let args: Vec = vec!["--norestart".into(), "--package".into(), package.into()]; let exe_path = std::env::current_exe()?; let work_dir: Option = None; // same as this process let process_handle = process::run_process_as_admin(&exe_path, args, work_dir, false)?; @@ -108,7 +107,7 @@ pub fn apply_package_impl(old_locator: &VelopackLocator, package: &PathBuf, run_ // } // fourth, we make as backup of the current dir to temp_path_old - info!("Backing up current dir to {}", &temp_path_old.to_string_lossy()); + info!("Backing up current dir to {:?}", &temp_path_old); shared::retry_io_ex(|| fs::rename(¤t_dir, &temp_path_old), 1000, 10) .context("Unable to start the update, because one or more running processes prevented it. Try again later, or if the issue persists, restart your computer.")?; @@ -121,7 +120,7 @@ pub fn apply_package_impl(old_locator: &VelopackLocator, package: &PathBuf, run_ // fifth, we try to replace the current dir with temp_path_new // if this fails we will yolo a rollback... - info!("Replacing current dir with {}", &temp_path_new.to_string_lossy()); + info!("Replacing current dir with {:?}", &temp_path_new); shared::retry_io_ex(|| fs::rename(&temp_path_new, ¤t_dir), 1000, 30) .context("Unable to complete the update, and the app was left in a broken state. You may need to re-install or repair this application manually.")?; diff --git a/src/bins/src/commands/install.rs b/src/bins/src/commands/install.rs index 0bb0c3e7..f19ea231 100644 --- a/src/bins/src/commands/install.rs +++ b/src/bins/src/commands/install.rs @@ -3,25 +3,24 @@ use crate::{ shared::{self}, windows, }; -use velopack::bundle::BundleZip; -use velopack::locator::*; use velopack::constants; +use velopack::locator::*; +use velopack::{bundle::BundleZip, wide_strings::string_to_wide}; +use ::windows::Win32::Storage::FileSystem::GetDiskFreeSpaceExW; use anyhow::{anyhow, bail, Result}; use pretty_bytes_rust::pretty_bytes; use std::{ + ffi::OsString, fs::{self}, path::{Path, PathBuf}, }; -use ::windows::core::PCWSTR; -use ::windows::Win32::Storage::FileSystem::GetDiskFreeSpaceExW; -pub fn install(pkg: &mut BundleZip, install_to: Option<&PathBuf>, start_args: Option>) -> Result<()> { +pub fn install(pkg: &mut BundleZip, install_to: Option<&PathBuf>, start_args: Option>) -> Result<()> { // find and parse nuspec info!("Reading package manifest..."); let app = pkg.read_manifest()?; - info!("Package manifest loaded successfully."); info!(" Package ID: {}", &app.id); info!(" Package Version: {}", &app.version); @@ -49,17 +48,15 @@ pub fn install(pkg: &mut BundleZip, install_to: Option<&PathBuf>, start_args: Op shared::retry_io(|| fs::create_dir_all(&root_path))?; } - let root_path_str = root_path.to_str().unwrap(); - info!("Installation Directory: {:?}", root_path_str); + info!("Installation Directory: {:?}", root_path); // do we have enough disk space? let (compressed_size, extracted_size) = pkg.calculate_size(); let required_space = compressed_size + extracted_size + (50 * 1000 * 1000); // archive + velopack overhead let mut free_space: u64 = 0; - let root_pcwstr = windows::strings::string_to_u16(root_path_str); - let root_pcwstr: PCWSTR = PCWSTR(root_pcwstr.as_ptr()); - if let Ok(()) = unsafe { GetDiskFreeSpaceExW(root_pcwstr, None, None, Some(&mut free_space)) } { + let root_pcwstr = string_to_wide(&root_path); + if let Ok(()) = unsafe { GetDiskFreeSpaceExW(root_pcwstr.as_pcwstr(), None, None, Some(&mut free_space)) } { if free_space < required_space { bail!( "{} requires at least {} disk space to be installed. There is only {} available.", @@ -85,7 +82,7 @@ pub fn install(pkg: &mut BundleZip, install_to: Option<&PathBuf>, start_args: Op bail!("This application ({}) does not support your CPU architecture.", &app.machine_architecture); } - let mut root_path_renamed = String::new(); + let mut root_path_renamed: Option = None; // does the target directory exist and have files? (eg. already installed) if !shared::is_dir_empty(&root_path) { // the target directory is not empty, and not dead @@ -100,20 +97,22 @@ pub fn install(pkg: &mut BundleZip, install_to: Option<&PathBuf>, start_args: Op anyhow!("Failed to stop application ({}), please close the application and try running the installer again.", z) })?; - root_path_renamed = format!("{}_{}", root_path_str, shared::random_string(16)); - info!("Renaming existing directory to '{}' to allow rollback...", root_path_renamed); + let renamed = root_path.with_extension(shared::random_string(16)); + info!("Renaming existing directory to '{:?}' to allow rollback...", renamed); - shared::retry_io(|| fs::rename(&root_path, &root_path_renamed)).map_err(|_| { + shared::retry_io(|| fs::rename(&root_path, &renamed)).map_err(|_| { anyhow!( "Failed to remove existing application directory, please close the application and try running the installer again. \ If the issue persists, try uninstalling first via Programs & Features, or restarting your computer." ) })?; + + root_path_renamed = Some(renamed); } info!("Preparing and cleaning installation directory..."); remove_dir_all::ensure_empty_dir(&root_path)?; - + info!("Acquiring lock..."); let paths = create_config_from_root_dir(&root_path); let locator = VelopackLocator::new_with_manifest(paths, app); @@ -134,17 +133,17 @@ pub fn install(pkg: &mut BundleZip, install_to: Option<&PathBuf>, start_args: Op if install_result.is_ok() { info!("Installation completed successfully!"); - if !root_path_renamed.is_empty() { + if let Some(renamed) = root_path_renamed { info!("Removing rollback directory..."); - let _ = shared::retry_io(|| fs::remove_dir_all(&root_path_renamed)); + let _ = shared::retry_io(|| remove_dir_all::remove_dir_all(&renamed)); } } else { error!("Installation failed!"); - if !root_path_renamed.is_empty() { + if let Some(renamed) = root_path_renamed { info!("Rolling back installation..."); let _ = shared::force_stop_package(&root_path); - let _ = shared::retry_io(|| fs::remove_dir_all(&root_path)); - let _ = shared::retry_io(|| fs::rename(&root_path_renamed, &root_path)); + let _ = shared::retry_io(|| remove_dir_all::remove_dir_all(&root_path)); + let _ = shared::retry_io(|| fs::rename(&renamed, &root_path)); } install_result?; } @@ -152,7 +151,12 @@ pub fn install(pkg: &mut BundleZip, install_to: Option<&PathBuf>, start_args: Op Ok(()) } -fn install_impl(pkg: &mut BundleZip, locator: &VelopackLocator, tx: &std::sync::mpsc::Sender, start_args: Option>) -> Result<()> { +fn install_impl( + pkg: &mut BundleZip, + locator: &VelopackLocator, + tx: &std::sync::mpsc::Sender, + start_args: Option>, +) -> Result<()> { info!("Starting installation!"); // all application paths diff --git a/src/bins/src/commands/patch.rs b/src/bins/src/commands/patch.rs index 92a54eb5..d78ba0cc 100644 --- a/src/bins/src/commands/patch.rs +++ b/src/bins/src/commands/patch.rs @@ -12,11 +12,11 @@ pub fn zstd_patch_single, P2: AsRef, P3: AsRef>(old_ let output_file = output_file.as_ref(); if !old_file.exists() { - bail!("Old file does not exist: {}", old_file.to_string_lossy()); + bail!("Old file does not exist: {:?}", old_file); } if !patch_file.exists() { - bail!("Patch file does not exist: {}", patch_file.to_string_lossy()); + bail!("Patch file does not exist: {:?}", patch_file); } let dict = fs::read(old_file)?; @@ -62,7 +62,7 @@ pub fn delta, P2: AsRef, P3: AsRef>( let output_file = output_file.as_ref().to_path_buf(); if !old_file.exists() { - bail!("Old file does not exist: {}", old_file.to_string_lossy()); + bail!("Old file does not exist: {:?}", old_file); } if delta_files.is_empty() { @@ -71,13 +71,13 @@ pub fn delta, P2: AsRef, P3: AsRef>( for delta_file in &delta_files { if !delta_file.exists() { - bail!("Delta file does not exist: {}", delta_file.to_string_lossy()); + bail!("Delta file does not exist: {:?}", delta_file); } } let time = simple_stopwatch::Stopwatch::start_new(); - info!("Extracting base package for delta patching: {}", temp_dir.to_string_lossy()); + info!("Extracting base package for delta patching: {:?}", temp_dir); let work_dir = temp_dir.join("_work"); fs::create_dir_all(&work_dir)?; fastzip::extract_to_directory(&old_file, &work_dir, None)?; @@ -85,7 +85,7 @@ pub fn delta, P2: AsRef, P3: AsRef>( info!("Base package extracted. {} delta packages to apply.", delta_files.len()); for (i, delta_file) in delta_files.iter().enumerate() { - info!("{}: extracting apply delta patch: {}", i, delta_file.to_string_lossy()); + info!("{}: extracting apply delta patch: {:?}", i, delta_file); let delta_dir = temp_dir.join(format!("delta_{}", i)); fs::create_dir_all(&delta_dir)?; fastzip::extract_to_directory(&delta_file, &delta_dir, None)?; @@ -152,7 +152,7 @@ pub fn delta, P2: AsRef, P3: AsRef>( } } - info!("All delta patches applied. Asembling output package at: {}", output_file.to_string_lossy()); + info!("All delta patches applied. Asembling output package at: {:?}", output_file); fastzip::compress_directory(&work_dir, &output_file, fastzip::CompressionLevel::fast())?; diff --git a/src/bins/src/commands/start.rs b/src/bins/src/commands/start.rs index f55e0b68..645936f4 100644 --- a/src/bins/src/commands/start.rs +++ b/src/bins/src/commands/start.rs @@ -1,22 +1,16 @@ use crate::shared::{self, OperationWait}; use anyhow::Result; +use std::ffi::OsString; use velopack::locator::LocationContext; -#[allow(unused_variables, unused_imports)] +#[allow(unused_variables)] pub fn start( wait: OperationWait, context: LocationContext, - exe_name: Option<&String>, - exe_args: Option>, - legacy_args: Option<&String>, + exe_name: Option<&OsString>, + exe_args: Option>, + legacy_args: Option<&OsString>, ) -> Result<()> { - use anyhow::bail; - - #[cfg(target_os = "windows")] - if legacy_args.is_some() && exe_args.is_some() { - bail!("Cannot use both legacy args and new args format."); - } - shared::operation_wait(wait); #[cfg(target_os = "windows")] diff --git a/src/bins/src/commands/start_windows_impl.rs b/src/bins/src/commands/start_windows_impl.rs index baf0dfaf..6f58c27a 100644 --- a/src/bins/src/commands/start_windows_impl.rs +++ b/src/bins/src/commands/start_windows_impl.rs @@ -4,19 +4,16 @@ use crate::{ windows as win, }; use anyhow::{anyhow, bail, Result}; -use std::os::windows::process::CommandExt; use std::{ - fs, - path::Path, - path::PathBuf, - process::Command as Process, + ffi::{OsStr, OsString}, + os::windows::process::CommandExt, }; -use velopack::{bundle::Manifest, constants}; +use std::{fs, path::Path, path::PathBuf, process::Command as Process}; use velopack::locator::{self, LocationContext, VelopackLocator}; +use velopack::{bundle::Manifest, constants}; use windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow; -enum LocatorResult -{ +enum LocatorResult { Normal(VelopackLocator), Legacy(PathBuf, Manifest), } @@ -34,18 +31,18 @@ impl LocatorResult { LocatorResult::Legacy(_, manifest) => manifest.clone(), } } - + pub fn get_current_dir(&self) -> PathBuf { match self { LocatorResult::Normal(locator) => locator.get_current_bin_dir(), LocatorResult::Legacy(path, _) => path.join("current"), } } - - pub fn get_exe_to_start(&self, name: Option<&String>) -> Result { + + pub fn get_exe_to_start>(&self, name: Option

) -> Result { let current_dir = self.get_current_dir(); if let Some(name) = name { - Ok(Path::new(¤t_dir).join(name)) + Ok(Path::new(¤t_dir).join(name.as_ref())) } else { match self { LocatorResult::Normal(locator) => Ok(locator.get_main_exe_path()), @@ -71,10 +68,10 @@ fn legacy_locator(context: LocationContext) -> Result { let parent_dir = my_exe.parent().expect("Unable to determine parent directory"); let packages_dir = parent_dir.join("packages"); if let Some((path, manifest)) = locator::find_latest_full_package(&packages_dir) { - info!("Found full package to read: {}", path.to_string_lossy()); + info!("Found full package to read: {:?}", path); Ok(LocatorResult::Legacy(parent_dir.to_path_buf(), manifest)) } else { - bail!("Unable to locate app manifest or full package in {}.", packages_dir.to_string_lossy()); + bail!("Unable to locate app manifest or full package in {:?}.", packages_dir); } } } @@ -82,10 +79,14 @@ fn legacy_locator(context: LocationContext) -> Result { pub fn start_impl( context: LocationContext, - exe_name: Option<&String>, - exe_args: Option>, - legacy_args: Option<&String>, + exe_name: Option<&OsString>, + exe_args: Option>, + legacy_args: Option<&OsString>, ) -> Result<()> { + if legacy_args.is_some() && exe_args.is_some() { + anyhow::bail!("Cannot use both legacy args and new args format at the same time."); + } + let locator = legacy_locator(context)?; let root_dir = locator.get_root_dir(); let manifest = locator.get_manifest(); @@ -113,17 +114,17 @@ pub fn start_impl( fn start_regular( locator: LocatorResult, - exe_name: Option<&String>, - exe_args: Option>, - legacy_args: Option<&String>, + exe_name: Option<&OsString>, + exe_args: Option>, + legacy_args: Option<&OsString>, ) -> Result<()> { - // we can't just run the normal start_package command, because legacy squirrel might provide + // we can't just run the normal start_package command, because legacy squirrel might provide // an "exe name" to restart which no longer exists in the package let exe_to_execute = locator.get_exe_to_start(exe_name)?; if !exe_to_execute.exists() { bail!("Unable to find executable to start: '{:?}'", exe_to_execute); } - + let current = locator.get_current_dir(); info!("About to launch: '{:?}' in dir '{:?}'", exe_to_execute, current); @@ -143,13 +144,14 @@ fn start_regular( fn try_legacy_migration(root_dir: &PathBuf, manifest: &Manifest) -> Result { info!("This is a legacy app. Will try and upgrade it now."); - + // if started by legacy Squirrel, the working dir of Update.exe may be inside the app-* folder, // meaning we can not clean up properly. std::env::set_current_dir(&root_dir)?; let path_config = locator::create_config_from_root_dir(root_dir); - let package = locator::find_latest_full_package(&path_config.PackagesDir).ok_or_else(|| anyhow!("Unable to find latest full package."))?; - + let package = + locator::find_latest_full_package(&path_config.PackagesDir).ok_or_else(|| anyhow!("Unable to find latest full package."))?; + warn!("This application is installed in a folder prefixed with 'app-'. Attempting to migrate..."); let _ = shared::force_stop_package(&root_dir); diff --git a/src/bins/src/commands/uninstall.rs b/src/bins/src/commands/uninstall.rs index 91009485..de444247 100644 --- a/src/bins/src/commands/uninstall.rs +++ b/src/bins/src/commands/uninstall.rs @@ -19,7 +19,7 @@ pub fn uninstall(locator: &VelopackLocator, delete_self: bool) -> Result<()> { // remove all shortcuts pointing to the app windows::remove_all_shortcuts_for_root_dir(&root_path); - info!("Removing directory '{}'", root_path.to_string_lossy()); + info!("Removing directory '{:?}'", root_path); let _ = remove_dir_all::remove_dir_contents(&root_path); if let Err(e) = windows::registry::remove_uninstall_entry(&locator) { diff --git a/src/bins/src/setup.rs b/src/bins/src/setup.rs index b6a29a57..31d15e53 100644 --- a/src/bins/src/setup.rs +++ b/src/bins/src/setup.rs @@ -7,6 +7,7 @@ extern crate log; use anyhow::{bail, Result}; use clap::{arg, value_parser, Command}; use memmap2::Mmap; +use std::ffi::OsString; use std::fs::File; use std::{env, path::PathBuf}; use velopack_bins::*; @@ -83,7 +84,7 @@ fn main_inner() -> Result<()> { let debug = matches.get_one::("debug"); let install_to = matches.get_one::("installto"); - let exe_args: Option> = matches.get_many::("EXE_ARGS").map(|v| v.map(|f| f.as_str()).collect()); + let exe_args = matches.get_many::("EXE_ARGS").map(|v| v.map(|f| f.to_os_string()).collect()); info!("Starting Velopack Setup ({})", env!("NGBV_VERSION")); info!(" Location: {:?}", env::current_exe()?); diff --git a/src/bins/src/shared/dialogs_windows.rs b/src/bins/src/shared/dialogs_windows.rs index 4b54c709..64cf7f86 100644 --- a/src/bins/src/shared/dialogs_windows.rs +++ b/src/bins/src/shared/dialogs_windows.rs @@ -1,10 +1,10 @@ use super::{dialogs_common::*, dialogs_const::*}; -use crate::windows::strings::string_to_wide; use anyhow::Result; use std::path::PathBuf; use velopack::{ bundle::Manifest, locator::{auto_locate_app_manifest, LocationContext}, + wide_strings::{string_to_wide, string_to_wide_opt}, }; use windows::{ core::HRESULT, @@ -74,7 +74,7 @@ pub fn show_uninstall_complete_with_errors_dialog(app_title: &str, log_path: Opt let setup_name = string_to_wide(format!("{} Uninstall", app_title)); let instruction = string_to_wide(format!("{} uninstall has completed with errors.", app_title)); let content = string_to_wide( - "There may be left-over files or directories on your system. You can attempt to remove these manually or re-install the application and try again.", + "There may be left-over files or directories on your system. You can attempt to remove these manually or re-install the application and try again." ); let mut config = TASKDIALOGCONFIG::default(); @@ -224,7 +224,7 @@ pub fn generate_confirm( ico: DialogIcon, ) -> Result { let hparent = unsafe { GetDesktopWindow() }; - let mut ok_text_buf = ok_text.map(string_to_wide); + let mut ok_text_buf = string_to_wide_opt(ok_text); let mut custom_btns = if let Some(ok_text_buf) = ok_text_buf.as_mut() { let td_btn = TASKDIALOG_BUTTON { nButtonID: IDOK.0, pszButtonText: ok_text_buf.as_pcwstr() }; vec![td_btn] @@ -246,7 +246,7 @@ pub fn generate_confirm( let title_buf = string_to_wide(title); tdc.pszWindowTitle = title_buf.as_pcwstr(); - let mut header_buf = header.map(string_to_wide); + let mut header_buf = string_to_wide_opt(header); if let Some(header_buf) = header_buf.as_mut() { tdc.pszMainInstruction = header_buf.as_pcwstr(); } diff --git a/src/bins/src/shared/util_osx.rs b/src/bins/src/shared/util_osx.rs index fa90cf7f..0e13d318 100644 --- a/src/bins/src/shared/util_osx.rs +++ b/src/bins/src/shared/util_osx.rs @@ -24,7 +24,7 @@ pub fn wait_for_parent_to_exit(ms_to_wait: u32) -> Result<()> { } pub fn force_stop_package>(root_dir: P) -> Result<()> { - let root_dir = root_dir.as_ref().to_string_lossy().to_string(); + let root_dir = root_dir.as_ref(); let command = format!("quit app \"{}\"", root_dir); Process::new("/usr/bin/osascript").arg("-e").arg(command).spawn().map_err(|z| anyhow!("Failed to stop application ({}).", z))?; Ok(()) diff --git a/src/bins/src/shared/util_windows.rs b/src/bins/src/shared/util_windows.rs index 5dbc0d84..de2767ed 100644 --- a/src/bins/src/shared/util_windows.rs +++ b/src/bins/src/shared/util_windows.rs @@ -1,4 +1,3 @@ -use crate::windows::strings; use ::windows::{ core::PWSTR, Win32::{ @@ -11,10 +10,11 @@ use regex::Regex; use semver::Version; use std::{ collections::HashMap, + ffi::OsString, fs, path::{Path, PathBuf}, }; -use velopack::{locator::VelopackLocator, process}; +use velopack::{locator::VelopackLocator, process, wide_strings::wide_to_os_string}; // https://github.com/nushell/nushell/blob/4458aae3d41517d74ce1507ad3e8cd94021feb16/crates/nu-system/src/windows.rs#L593 fn get_pids() -> Result> { @@ -55,12 +55,9 @@ unsafe fn get_processes_running_in_directory>(dir: P) -> Result>(root_dir: P) -> Result<()> { Ok(()) } -pub fn start_package(locator: &VelopackLocator, exe_args: Option>, set_env: Option<&str>) -> Result<()> { +pub fn start_package(locator: &VelopackLocator, exe_args: Option>, set_env: Option<&str>) -> Result<()> { let current = locator.get_current_bin_dir(); let exe_to_execute = locator.get_main_exe_path(); if !exe_to_execute.exists() { - bail!("Unable to find executable to start: '{}'", exe_to_execute.to_string_lossy()); + bail!("Unable to find executable to start: '{:?}'", exe_to_execute); } - let args: Vec = exe_args.unwrap_or_default().iter().map(|s| s.to_string()).collect(); let mut environment = HashMap::new(); if let Some(env_var) = set_env { debug!("Setting environment variable: {}={}", env_var, "true"); environment.insert(env_var.to_string(), "true".to_string()); } - process::run_process(exe_to_execute, args, Some(current), true, Some(environment))?; + process::run_process(exe_to_execute, exe_args.unwrap_or_default(), Some(current), true, Some(environment))?; Ok(()) } diff --git a/src/bins/src/stub.rs b/src/bins/src/stub.rs index 81ef9d87..5ad69b5c 100644 --- a/src/bins/src/stub.rs +++ b/src/bins/src/stub.rs @@ -37,7 +37,7 @@ fn main() -> ExitCode { args.insert(0, "start".to_owned()); args.insert(1, "--".to_owned()); - info!("Stub about to start Update.exe ({}) with args: {:?}", update_exe.to_string_lossy(), args); + info!("Stub about to start Update.exe ({:?}) with args: {:?}", update_exe, args); const CREATE_NO_WINDOW: u32 = 0x08000000; match Process::new(update_exe).args(args).creation_flags(CREATE_NO_WINDOW).spawn() { diff --git a/src/bins/src/update.rs b/src/bins/src/update.rs index fe84e0a5..94f20635 100644 --- a/src/bins/src/update.rs +++ b/src/bins/src/update.rs @@ -6,6 +6,7 @@ extern crate log; use anyhow::{anyhow, bail, Result}; use clap::{arg, value_parser, ArgAction, ArgMatches, Command}; +use std::ffi::OsString; use std::{env, path::PathBuf}; use velopack::locator::{auto_locate_app_manifest, LocationContext}; use velopack::logging::*; @@ -22,15 +23,15 @@ fn root_command() -> Command { .arg(arg!(-w --wait "Wait for the parent process to terminate before applying the update").hide(true)) .arg(arg!(--waitPid "Wait for the specified process to terminate before applying the update").value_parser(value_parser!(u32))) .arg(arg!(-p --package "Update package to apply").value_parser(value_parser!(PathBuf))) - .arg(arg!([EXE_ARGS] "Arguments to pass to the started executable. Must be preceded by '--'.").required(false).last(true).num_args(0..)) + .arg(arg!([EXE_ARGS] "Arguments to pass to the started executable. Must be preceded by '--'.").required(false).last(true).num_args(0..).value_parser(value_parser!(OsString))) ) .subcommand(Command::new("start") .about("Starts the currently installed version of the application") - .arg(arg!(-a --args "Legacy args format").aliases(vec!["processStartArgs", "process-start-args"]).hide(true).allow_hyphen_values(true).num_args(1)) + .arg(arg!(-a --args "Legacy args format").aliases(vec!["processStartArgs", "process-start-args"]).hide(true).allow_hyphen_values(true).num_args(1).value_parser(value_parser!(OsString))) .arg(arg!(-w --wait "Wait for the parent process to terminate before starting the application").hide(true)) .arg(arg!(--waitPid "Wait for the specified process to terminate before applying the update").value_parser(value_parser!(u32))) - .arg(arg!([EXE_NAME] "The optional name of the binary to execute")) - .arg(arg!([EXE_ARGS] "Arguments to pass to the started executable. Must be preceded by '--'.").required(false).last(true).num_args(0..)) + .arg(arg!([EXE_NAME] "The optional name of the binary to execute").value_parser(value_parser!(OsString))) + .arg(arg!([EXE_ARGS] "Arguments to pass to the started executable. Must be preceded by '--'.").required(false).last(true).num_args(0..).value_parser(value_parser!(OsString))) .long_flag_aliases(vec!["processStart", "processStartAndWait"]) ) .subcommand(Command::new("patch") @@ -158,8 +159,8 @@ fn main() -> Result<()> { info!("--"); info!("Starting Velopack Updater ({})", env!("NGBV_VERSION")); - info!(" Location: {}", env::current_exe()?.to_string_lossy()); - info!(" CWD: {}", env::current_dir()?.to_string_lossy()); + info!(" Location: {:?}", env::current_exe()?); + info!(" CWD: {:?}", env::current_dir()?); info!(" Verbose: {}", verbose); info!(" Silent: {}", silent); info!(" Log File: {:?}", log_file); @@ -220,11 +221,11 @@ fn patch(_context: LocationContext, matches: &ArgMatches) -> Result<()> { Ok(()) } -fn get_exe_args(matches: &ArgMatches) -> Option> { - matches.get_many::("EXE_ARGS").map(|v| v.map(|f| f.as_str()).collect()) +fn get_exe_args(matches: &ArgMatches) -> Option> { + matches.get_many::("EXE_ARGS").map(|v| v.map(|f| f.to_os_string()).collect()) } -fn get_apply_args(matches: &ArgMatches) -> (OperationWait, bool, Option<&PathBuf>, Option>) { +fn get_apply_args(matches: &ArgMatches) -> (OperationWait, bool, Option<&PathBuf>, Option>) { let restart = !get_flag_or_false(&matches, "norestart"); let package = matches.get_one::("package"); let exe_args = get_exe_args(matches); @@ -246,9 +247,9 @@ fn apply(context: LocationContext, matches: &ArgMatches) -> Result<()> { Ok(()) } -fn get_start_args(matches: &ArgMatches) -> (OperationWait, Option<&String>, Option<&String>, Option>) { - let legacy_args = matches.get_one::("args"); - let exe_name = matches.get_one::("EXE_NAME"); +fn get_start_args(matches: &ArgMatches) -> (OperationWait, Option<&OsString>, Option<&OsString>, Option>) { + let legacy_args = matches.get_one::("args"); + let exe_name = matches.get_one::("EXE_NAME"); let exe_args = get_exe_args(matches); let wait = get_op_wait(&matches); (wait, exe_name, legacy_args, exe_args) @@ -346,7 +347,7 @@ fn test_start_command_supports_legacy_commands() { let matches = try_parse_command_line_matches(command.iter().map(|s| s.to_string()).collect()).unwrap(); let (wait_for_parent, exe_name, legacy_args, exe_args) = get_start_args(matches.subcommand_matches("start").unwrap()); assert_eq!(wait_for_parent, OperationWait::NoWait); - assert_eq!(exe_name, Some(&"hello.exe".to_string())); + assert_eq!(exe_name, Some(&"hello.exe".into())); assert_eq!(legacy_args, None); assert_eq!(exe_args, None); @@ -354,7 +355,7 @@ fn test_start_command_supports_legacy_commands() { let matches = try_parse_command_line_matches(command.iter().map(|s| s.to_string()).collect()).unwrap(); let (wait_for_parent, exe_name, legacy_args, exe_args) = get_start_args(matches.subcommand_matches("start").unwrap()); assert_eq!(wait_for_parent, OperationWait::NoWait); - assert_eq!(exe_name, Some(&"hello.exe".to_string())); + assert_eq!(exe_name, Some(&"hello.exe".into())); assert_eq!(legacy_args, None); assert_eq!(exe_args, None); @@ -362,7 +363,7 @@ fn test_start_command_supports_legacy_commands() { let matches = try_parse_command_line_matches(command.iter().map(|s| s.to_string()).collect()).unwrap(); let (wait_for_parent, exe_name, legacy_args, exe_args) = get_start_args(matches.subcommand_matches("start").unwrap()); assert_eq!(wait_for_parent, OperationWait::WaitParent); - assert_eq!(exe_name, Some(&"hello.exe".to_string())); + assert_eq!(exe_name, Some(&"hello.exe".into())); assert_eq!(legacy_args, None); assert_eq!(exe_args, None); @@ -370,40 +371,40 @@ fn test_start_command_supports_legacy_commands() { let matches = try_parse_command_line_matches(command.iter().map(|s| s.to_string()).collect()).unwrap(); let (wait_for_parent, exe_name, legacy_args, exe_args) = get_start_args(matches.subcommand_matches("start").unwrap()); assert_eq!(wait_for_parent, OperationWait::WaitParent); - assert_eq!(exe_name, Some(&"hello.exe".to_string())); + assert_eq!(exe_name, Some(&"hello.exe".into())); assert_eq!(legacy_args, None); - assert_eq!(exe_args, Some(vec!["Foo=Bar"])); + assert_eq!(exe_args, Some(vec!["Foo=Bar".into()])); let command = vec!["Update.exe", "--processStartAndWait", "hello.exe", "-a", "myarg"]; let matches = try_parse_command_line_matches(command.iter().map(|s| s.to_string()).collect()).unwrap(); let (wait_for_parent, exe_name, legacy_args, exe_args) = get_start_args(matches.subcommand_matches("start").unwrap()); assert_eq!(wait_for_parent, OperationWait::WaitParent); - assert_eq!(exe_name, Some(&"hello.exe".to_string())); - assert_eq!(legacy_args, Some(&"myarg".to_string())); + assert_eq!(exe_name, Some(&"hello.exe".into())); + assert_eq!(legacy_args, Some(&"myarg".into())); assert_eq!(exe_args, None); let command = vec!["Update.exe", "--processStartAndWait", "hello.exe", "-a", "myarg"]; let matches = try_parse_command_line_matches(command.iter().map(|s| s.to_string()).collect()).unwrap(); let (wait_for_parent, exe_name, legacy_args, exe_args) = get_start_args(matches.subcommand_matches("start").unwrap()); assert_eq!(wait_for_parent, OperationWait::WaitParent); - assert_eq!(exe_name, Some(&"hello.exe".to_string())); - assert_eq!(legacy_args, Some(&"myarg".to_string())); + assert_eq!(exe_name, Some(&"hello.exe".into())); + assert_eq!(legacy_args, Some(&"myarg".into())); assert_eq!(exe_args, None); let command = vec!["Update.exe", "--processStartAndWait", "hello.exe", "--processStartArgs", "myarg"]; let matches = try_parse_command_line_matches(command.iter().map(|s| s.to_string()).collect()).unwrap(); let (wait_for_parent, exe_name, legacy_args, exe_args) = get_start_args(matches.subcommand_matches("start").unwrap()); assert_eq!(wait_for_parent, OperationWait::WaitParent); - assert_eq!(exe_name, Some(&"hello.exe".to_string())); - assert_eq!(legacy_args, Some(&"myarg".to_string())); + assert_eq!(exe_name, Some(&"hello.exe".into())); + assert_eq!(legacy_args, Some(&"myarg".into())); assert_eq!(exe_args, None); let command = vec!["Update.exe", "--processStartAndWait", "hello.exe", "--process-start-args", "myarg"]; let matches = try_parse_command_line_matches(command.iter().map(|s| s.to_string()).collect()).unwrap(); let (wait_for_parent, exe_name, legacy_args, exe_args) = get_start_args(matches.subcommand_matches("start").unwrap()); assert_eq!(wait_for_parent, OperationWait::WaitParent); - assert_eq!(exe_name, Some(&"hello.exe".to_string())); - assert_eq!(legacy_args, Some(&"myarg".to_string())); + assert_eq!(exe_name, Some(&"hello.exe".into())); + assert_eq!(legacy_args, Some(&"myarg".into())); assert_eq!(exe_args, None); let command = vec!["Update.exe", "--processStartAndWait", "-a", "myarg"]; @@ -411,7 +412,7 @@ fn test_start_command_supports_legacy_commands() { let (wait_for_parent, exe_name, legacy_args, exe_args) = get_start_args(matches.subcommand_matches("start").unwrap()); assert_eq!(wait_for_parent, OperationWait::WaitParent); assert_eq!(exe_name, None); - assert_eq!(legacy_args, Some(&"myarg".to_string())); + assert_eq!(legacy_args, Some(&"myarg".into())); assert_eq!(exe_args, None); let command = vec!["Update.exe", "--processStartAndWait", "-a", "-- -c \" asda --aasd"]; @@ -419,6 +420,6 @@ fn test_start_command_supports_legacy_commands() { let (wait_for_parent, exe_name, legacy_args, exe_args) = get_start_args(matches.subcommand_matches("start").unwrap()); assert_eq!(wait_for_parent, OperationWait::WaitParent); assert_eq!(exe_name, None); - assert_eq!(legacy_args, Some(&"-- -c \" asda --aasd".to_string())); + assert_eq!(legacy_args, Some(&"-- -c \" asda --aasd".into())); assert_eq!(exe_args, None); } diff --git a/src/bins/src/windows/known_path.rs b/src/bins/src/windows/known_path.rs index ff0ca725..54c67fa5 100644 --- a/src/bins/src/windows/known_path.rs +++ b/src/bins/src/windows/known_path.rs @@ -1,5 +1,6 @@ -use anyhow::Result; -use std::path::Path; +use anyhow::{bail, Result}; +use std::path::PathBuf; +use velopack::wide_strings::wide_to_os_string; use windows::{ core::GUID, Win32::UI::Shell::{ @@ -8,54 +9,60 @@ use windows::{ }, }; -fn get_known_folder(rfid: *const GUID) -> Result { +fn get_known_folder(rfid: *const GUID) -> Result { unsafe { let flag = windows::Win32::UI::Shell::KNOWN_FOLDER_FLAG(0); let result = SHGetKnownFolderPath(rfid, flag, None)?; - super::strings::u16_to_string(result) + if result.is_null() { + bail!("Failed to get known folder path (SHGetKnownFolderPath returned null)"); + } + + let str = wide_to_os_string(result); + let path = PathBuf::from(str); + Ok(path) } } -pub fn get_local_app_data() -> Result { +pub fn get_local_app_data() -> Result { get_known_folder(&FOLDERID_LocalAppData) } -pub fn get_roaming_app_data() -> Result { +pub fn get_roaming_app_data() -> Result { get_known_folder(&FOLDERID_RoamingAppData) } -pub fn get_user_desktop() -> Result { +pub fn get_user_desktop() -> Result { get_known_folder(&FOLDERID_Desktop) } -pub fn get_user_profile() -> Result { +pub fn get_user_profile() -> Result { get_known_folder(&FOLDERID_Profile) } -pub fn get_start_menu() -> Result { +pub fn get_start_menu() -> Result { let start_menu = get_known_folder(&FOLDERID_StartMenu)?; - let programs_path = Path::new(&start_menu).join("Programs"); - Ok(programs_path.to_string_lossy().to_string()) + let programs_path = start_menu.join("Programs"); + Ok(programs_path) } -pub fn get_startup() -> Result { +pub fn get_startup() -> Result { get_known_folder(&FOLDERID_Startup) } -pub fn get_downloads() -> Result { +pub fn get_downloads() -> Result { get_known_folder(&FOLDERID_Downloads) } -pub fn get_program_files_x64() -> Result { +pub fn get_program_files_x64() -> Result { get_known_folder(&FOLDERID_ProgramFilesX64) } -pub fn get_program_files_x86() -> Result { +pub fn get_program_files_x86() -> Result { get_known_folder(&FOLDERID_ProgramFilesX86) } -pub fn get_user_pinned() -> Result { +pub fn get_user_pinned() -> Result { let pinned_str = get_roaming_app_data()?; - let pinned_path = Path::new(&pinned_str).join("Microsoft\\Internet Explorer\\Quick Launch\\User Pinned"); - Ok(pinned_path.to_string_lossy().to_string()) + let pinned_path = pinned_str.join("Microsoft").join("Internet Explorer").join("Quick Launch").join("User Pinned"); + Ok(pinned_path) } diff --git a/src/bins/src/windows/mod.rs b/src/bins/src/windows/mod.rs index 0ec60254..66f5dcaa 100644 --- a/src/bins/src/windows/mod.rs +++ b/src/bins/src/windows/mod.rs @@ -5,7 +5,6 @@ pub mod runtimes; pub mod splash; pub mod known_path; -pub mod strings; pub mod registry; pub mod webview2; diff --git a/src/bins/src/windows/prerequisite.rs b/src/bins/src/windows/prerequisite.rs index ab79d8d1..88767936 100644 --- a/src/bins/src/windows/prerequisite.rs +++ b/src/bins/src/windows/prerequisite.rs @@ -1,9 +1,7 @@ use super::{runtimes, splash}; use crate::shared::dialogs; -use velopack::{bundle, download}; - use anyhow::Result; -use std::path::Path; +use velopack::{bundle, download}; pub fn prompt_and_install_all_missing(app: &bundle::Manifest, updating_from: Option<&semver::Version>) -> Result { info!("Checking application pre-requisites..."); @@ -39,7 +37,6 @@ pub fn prompt_and_install_all_missing(app: &bundle::Manifest, updating_from: Opt } let downloads = super::known_path::get_downloads()?; - let downloads = Path::new(downloads.as_str()); info!("Downloading {} missing pre-requisites...", missing.len()); let quiet = dialogs::get_silent(); @@ -47,15 +44,19 @@ pub fn prompt_and_install_all_missing(app: &bundle::Manifest, updating_from: Opt for i in 0..missing.len() { let dep = &missing[i]; let url = dep.get_download_url()?; - let exe_name = downloads.join(dep.get_exe_name()); + let exe_path = downloads.join(dep.get_exe_name()); - if !exe_name.exists() { - let window_title = if updating_from.is_some() { format!("{} Update", dep.display_name()) } else { format!("{} Setup", dep.display_name()) }; + if !exe_path.exists() { + let window_title = if updating_from.is_some() { + format!("{} Update", dep.display_name()) + } else { + format!("{} Setup", dep.display_name()) + }; let content = format!("Downloading {}...", dep.display_name()); info!(" {}", content); let tx = splash::show_progress_dialog(window_title, content); - let result = download::download_url_to_file(&url, &exe_name.to_str().unwrap(), |p| { + let result = download::download_url_to_file(&url, &exe_path, |p| { let _ = tx.send(p); }); @@ -64,7 +65,7 @@ pub fn prompt_and_install_all_missing(app: &bundle::Manifest, updating_from: Opt } info!(" Installing {}...", dep.display_name()); - let result = dep.install(exe_name.to_str().unwrap(), quiet)?; + let result = dep.install(&exe_path, quiet)?; if result == runtimes::RuntimeInstallResult::RestartRequired { warn!("A restart is required to complete the installation of {}.", dep.display_name()); dialogs::show_restart_required(&app); diff --git a/src/bins/src/windows/registry.rs b/src/bins/src/windows/registry.rs index ba03f6e2..09c1aba0 100644 --- a/src/bins/src/windows/registry.rs +++ b/src/bins/src/windows/registry.rs @@ -1,5 +1,6 @@ use anyhow::Result; use chrono::{Datelike, Local as DateTime}; +use std::ffi::OsString; use velopack::locator::VelopackLocator; use winreg::{enums::*, RegKey}; @@ -12,9 +13,9 @@ pub fn write_uninstall_entry(locator: &VelopackLocator) -> Result<()> { let app_title = locator.get_manifest_title(); let app_authors = locator.get_manifest_authors(); - let root_path_str = locator.get_root_dir_as_string(); - let main_exe_path = locator.get_main_exe_path_as_string(); - let updater_path = locator.get_update_path_as_string(); + let root_path = locator.get_root_dir(); + let main_exe_path = locator.get_main_exe_path(); + let updater_path = locator.get_update_path(); let folder_size = fs_extra::dir::get_size(locator.get_current_bin_dir()).unwrap_or(0); let folder_size_kb = folder_size / 1024; @@ -23,8 +24,13 @@ pub fn write_uninstall_entry(locator: &VelopackLocator) -> Result<()> { let now = DateTime::now(); let formatted_date = format!("{}{:02}{:02}", now.year(), now.month(), now.day()); - let uninstall_cmd = format!("\"{}\" --uninstall", updater_path); - let uninstall_quiet: String = format!("\"{}\" --uninstall --silent", updater_path); + let mut uninstall_cmd = OsString::from("\""); + uninstall_cmd.push(&updater_path); + uninstall_cmd.push("\" --uninstall"); + + let mut uninstall_quiet = OsString::from("\""); + uninstall_quiet.push(&updater_path); + uninstall_quiet.push("\" --uninstall --silent"); let hkcu = RegKey::predef(HKEY_CURRENT_USER); let (reg_uninstall, _reg_uninstall_disp) = hkcu.create_subkey(UNINSTALL_REGISTRY_KEY)?; @@ -33,11 +39,11 @@ pub fn write_uninstall_entry(locator: &VelopackLocator) -> Result<()> { let u32true = 1u32; let language = 0x0409u32; - reg_app.set_value("DisplayIcon", &main_exe_path)?; + reg_app.set_value("DisplayIcon", &main_exe_path.as_os_str())?; reg_app.set_value("DisplayName", &app_title)?; reg_app.set_value("DisplayVersion", &short_version)?; reg_app.set_value("InstallDate", &formatted_date)?; - reg_app.set_value("InstallLocation", &root_path_str)?; + reg_app.set_value("InstallLocation", &root_path.as_os_str())?; reg_app.set_value("Publisher", &app_authors)?; reg_app.set_value("QuietUninstallString", &uninstall_quiet)?; reg_app.set_value("UninstallString", &uninstall_cmd)?; diff --git a/src/bins/src/windows/runtimes.rs b/src/bins/src/windows/runtimes.rs index 116a574b..5a18ceeb 100644 --- a/src/bins/src/windows/runtimes.rs +++ b/src/bins/src/windows/runtimes.rs @@ -67,7 +67,7 @@ pub enum RuntimeInstallResult { RestartRequired, } -fn def_installer_routine(installer_path: &str, quiet: bool) -> Result { +fn def_installer_routine(installer_path: &Path, quiet: bool) -> Result { let mut args = Vec::new(); args.push("/norestart"); if quiet { @@ -77,7 +77,7 @@ fn def_installer_routine(installer_path: &str, quiet: bool) -> Result &str; fn is_installed(&self) -> bool; fn get_download_url(&self) -> Result; - fn install(&self, installer_path: &str, quiet: bool) -> Result; + fn install(&self, installer_path: &Path, quiet: bool) -> Result; } #[derive(Clone, Debug)] @@ -143,7 +143,7 @@ impl RuntimeInfo for FullFrameworkInfo { } } - fn install(&self, installer_path: &str, quiet: bool) -> Result { + fn install(&self, installer_path: &Path, quiet: bool) -> Result { def_installer_routine(installer_path, quiet) } } @@ -204,7 +204,7 @@ impl RuntimeInfo for VCRedistInfo { false } - fn install(&self, installer_path: &str, quiet: bool) -> Result { + fn install(&self, installer_path: &Path, quiet: bool) -> Result { def_installer_routine(installer_path, quiet) } } @@ -460,7 +460,7 @@ impl RuntimeInfo for DotnetInfo { false } - fn install(&self, installer_path: &str, quiet: bool) -> Result { + fn install(&self, installer_path: &Path, quiet: bool) -> Result { def_installer_routine(installer_path, quiet) } } @@ -549,14 +549,14 @@ impl RuntimeInfo for WebView2Info { } } - fn install(&self, installer_path: &str, quiet: bool) -> Result { + fn install(&self, installer_path: &Path, quiet: bool) -> Result { let args = if quiet { vec!["/silent", "/install"] } else { vec!["/install"] }; - info!("Running installer: '{}', args={:?}", installer_path, args); + info!("Running installer: '{:?}', args={:?}", installer_path, args); let mut cmd = Process::new(installer_path).args(&args).spawn()?; let result: i32 = cmd.wait()?.code().ok_or_else(|| anyhow!("Unable to get installer exit code."))?; diff --git a/src/bins/src/windows/shortcuts.rs b/src/bins/src/windows/shortcuts.rs index bbcfcfb5..85e64b8c 100644 --- a/src/bins/src/windows/shortcuts.rs +++ b/src/bins/src/windows/shortcuts.rs @@ -5,8 +5,8 @@ use std::time::Duration; use anyhow::{anyhow, bail, Result}; use glob::glob; use same_file::is_same_file; -use velopack::{locator::ShortcutLocationFlags, locator::VelopackLocator}; -use windows::core::{Interface, GUID, PCWSTR}; +use velopack::{locator::ShortcutLocationFlags, locator::VelopackLocator, wide_strings::*}; +use windows::core::{Interface, GUID}; use windows::Win32::Storage::EnhancedStorage::PKEY_AppUserModel_ID; use windows::Win32::System::Com::{ CoCreateInstance, CoInitializeEx, CoUninitialize, IPersistFile, StructuredStorage::InitPropVariantFromStringVector, @@ -17,7 +17,7 @@ use windows::Win32::UI::Shell::{ }; use crate::shared as util; -use crate::windows::{known_path as known, strings::*}; +use crate::windows::known_path as known; // https://github.com/vaginessa/PWAsForFirefox/blob/fba68dbcc7ca27b970dc5a278ebdad32e0ab3c83/native/src/integrations/implementation/windows.rs#L28 @@ -52,7 +52,11 @@ unsafe fn create_instance(clsid: &GUID) -> Result { } fn get_shortcut_filename(app_id: &str, app_title: &str) -> String { - let name = if app_title.is_empty() { app_id.to_owned() } else { app_title.to_owned() }; + let name = if app_title.is_empty() { + app_id.to_owned() + } else { + app_title.to_owned() + }; let shortcut_file_name = name + ".lnk"; shortcut_file_name } @@ -96,8 +100,8 @@ unsafe fn unsafe_update_app_manifest_lnks(next_app: &VelopackLocator, previous_a let app_title = next_app.get_manifest_title(); let app_authors = next_app.get_manifest_authors(); let app_model_id: Option = next_app.get_manifest_shortcut_amuid(); - let app_main_exe = next_app.get_main_exe_path_as_string(); - let app_work_dir = next_app.get_current_bin_dir_as_string(); + let app_main_exe = next_app.get_main_exe_path(); + let app_work_dir = next_app.get_current_bin_dir(); info!("App Model ID: {:?}", app_model_id); let mut current_shortcuts = unsafe_get_shortcuts_for_root_dir(root_path); @@ -113,7 +117,7 @@ unsafe fn unsafe_update_app_manifest_lnks(next_app: &VelopackLocator, previous_a // set the target path to the main exe if it is missing or incorrect if target_option.is_none() || !PathBuf::from(target_option.unwrap()).exists() { - warn!("Shortcut {} target does not exist, updating to mainExe and setting workdir to current.", lnk.get_link_path()); + warn!("Shortcut {:?} target does not exist, updating to mainExe and setting workdir to current.", lnk.get_link_path()); if let Err(e) = lnk.set_target_path(&app_main_exe) { warn!("Failed to update shortcut target: {}", e); } @@ -213,7 +217,7 @@ unsafe fn unsafe_update_app_manifest_lnks(next_app: &VelopackLocator, previous_a break; } - if let Err(e) = lnk.save_as(&path.to_string_lossy()) { + if let Err(e) = lnk.save_as(&path) { warn!("Failed to save shortcut: {}", e); break; } @@ -280,33 +284,34 @@ unsafe fn unsafe_find_best_rename_candidates>( unsafe fn unsafe_get_shortcuts_for_root_dir>(root_dir: P) -> Vec<(ShortcutLocationFlags, Lnk)> { let root_dir = root_dir.as_ref(); - info!("Searching for shortcuts containing root: '{}'", root_dir.to_string_lossy()); + info!("Searching for shortcuts containing root: '{:?}'", root_dir); let mut search_paths = Vec::new(); match known::get_user_desktop() { - Ok(user_desktop) => search_paths.push((ShortcutLocationFlags::DESKTOP, format!("{}/*.lnk", user_desktop))), + Ok(user_desktop) => search_paths.push((ShortcutLocationFlags::DESKTOP, user_desktop, "/*.lnk")), Err(e) => error!("Failed to get user desktop directory, it will not be searched: {}", e), } match known::get_startup() { - Ok(startup) => search_paths.push((ShortcutLocationFlags::STARTUP, format!("{}/*.lnk", startup))), + Ok(startup) => search_paths.push((ShortcutLocationFlags::STARTUP, startup, "/*.lnk")), Err(e) => error!("Failed to get startup directory, it will not be searched: {}", e), } match known::get_start_menu() { // this handles START_MENU and START_MENU_ROOT - Ok(start_menu) => search_paths.push((ShortcutLocationFlags::START_MENU, format!("{}/**/*.lnk", start_menu))), + Ok(start_menu) => search_paths.push((ShortcutLocationFlags::START_MENU, start_menu, "/**/*.lnk")), Err(e) => error!("Failed to get start menu directory, it will not be searched: {}", e), } match known::get_user_pinned() { - Ok(user_pinned) => search_paths.push((ShortcutLocationFlags::USER_PINNED, format!("{}/**/*.lnk", user_pinned))), + Ok(user_pinned) => search_paths.push((ShortcutLocationFlags::USER_PINNED, user_pinned, "/**/*.lnk")), Err(e) => error!("Failed to get user pinned directory, it will not be searched: {}", e), } let mut paths: Vec<(ShortcutLocationFlags, Lnk)> = Vec::new(); - for (flag, search_glob) in search_paths { + for (flag, folder_root, search_pattern) in search_paths { + let search_glob = folder_root.to_string_lossy().to_string() + search_pattern; info!("Searching for shortcuts in: {:?} ({})", flag, search_glob); if let Ok(glob_paths) = glob(&search_glob) { for path in glob_paths.filter_map(Result::ok) { @@ -315,16 +320,16 @@ unsafe fn unsafe_get_shortcuts_for_root_dir>(root_dir: P) -> Vec< Ok(properties) => { if let Ok(target) = properties.get_target_path() { if super::is_sub_path(&target, root_dir).unwrap_or(false) { - info!("Selected shortcut for update '{}' because target '{}' matched.", path.to_string_lossy(), target); + info!("Selected shortcut for update '{:?}' because target '{:?}' matched.", path, target); paths.push((flag, properties)); } } else if let Ok(work_dir) = properties.get_working_directory() { if super::is_sub_path(&work_dir, root_dir).unwrap_or(false) { - info!("Selected shortcut for update '{}' because work_dir '{}' matched.", path.to_string_lossy(), work_dir); + info!("Selected shortcut for update '{:?}' because work_dir '{:?}' matched.", path, work_dir); paths.push((flag, properties)); } } else { - warn!("Could not resolve target or work_dir for shortcut '{}'.", path.to_string_lossy()); + warn!("Could not resolve target or work_dir for shortcut '{:?}'.", path); } } Err(e) => { @@ -341,7 +346,7 @@ unsafe fn unsafe_remove_all_shortcuts_for_root_dir>(root_dir: P) let shortcuts = unsafe_get_shortcuts_for_root_dir(root_dir); for (flag, properties) in shortcuts { let path = properties.get_link_path(); - info!("Removing shortcut '{}' ({:?}).", path, flag); + info!("Removing shortcut '{:?}' ({:?}).", path, flag); let remove_parent_if_empty = flag == ShortcutLocationFlags::START_MENU; if let Err(e) = unsafe_delete_lnk_file(&path, remove_parent_if_empty) { warn!("Failed to remove shortcut: {}", e); @@ -383,8 +388,8 @@ unsafe fn unsafe_unpin_lnk_from_start>(path: P) -> Result<()> { return Ok(()); } - let path = string_to_u16(path.to_string_lossy()); - let item_result: IShellItem = SHCreateItemFromParsingName(PCWSTR(path.as_ptr()), None)?; + let path = string_to_wide(path); + let item_result: IShellItem = SHCreateItemFromParsingName(path.as_pcwstr(), None)?; let pinned_list: IStartMenuPinnedList = create_instance(&StartMenuPin)?; pinned_list.RemoveFromList(&item_result)?; Ok(()) @@ -422,89 +427,86 @@ where struct Lnk { me: IShellLinkW, pf: IPersistFile, - my_path: String, + my_path: PathBuf, } #[allow(dead_code)] impl Lnk { - pub unsafe fn get_link_path(&self) -> String { + pub unsafe fn get_link_path(&self) -> PathBuf { self.my_path.clone() } pub unsafe fn get_arguments(&self) -> Result { let mut pszargs = [0u16; 1024]; self.me.GetArguments(&mut pszargs)?; - let args = u16_to_string(pszargs)?; + let args = wide_to_string(pszargs)?; Ok(args) } pub unsafe fn get_description(&self) -> Result { let mut pszdesc = [0u16; 1024]; self.me.GetDescription(&mut pszdesc)?; - let desc = u16_to_string(pszdesc)?; + let desc = wide_to_string(pszdesc)?; Ok(desc) } - pub unsafe fn get_icon_location(&self) -> Result<(String, i32)> { + pub unsafe fn get_icon_location(&self) -> Result<(PathBuf, i32)> { let mut pszfile = [0u16; 1024]; let mut pindex = 0; self.me.GetIconLocation(&mut pszfile, &mut pindex)?; - let icon = u16_to_string(pszfile)?; + let icon = wide_to_os_string(pszfile); + let icon = PathBuf::from(icon); Ok((icon, pindex)) } - pub unsafe fn get_target_path(&self) -> Result { + pub unsafe fn get_target_path(&self) -> Result { let mut pszfile = [0u16; 1024]; self.me.GetPath(&mut pszfile, std::ptr::null_mut(), 0)?; - let target = u16_to_string(pszfile)?; + let target = wide_to_os_string(pszfile); + let target = PathBuf::from(target); Ok(target) } - pub unsafe fn get_working_directory(&self) -> Result { + pub unsafe fn get_working_directory(&self) -> Result { let mut pszdir = [0u16; 1024]; self.me.GetWorkingDirectory(&mut pszdir)?; - let work_dir = u16_to_string(pszdir)?; + let work_dir = wide_to_os_string(pszdir); + let work_dir = PathBuf::from(work_dir); Ok(work_dir) } - pub unsafe fn set_arguments(&mut self, path: &str) -> Result<()> { - let args = string_to_u16(path); - let args = PCWSTR(args.as_ptr()); - Ok(self.me.SetArguments(args)?) + pub unsafe fn set_arguments>(&mut self, path: P) -> Result<()> { + let args = string_to_wide(path.as_ref()); + Ok(self.me.SetArguments(args.as_pcwstr())?) } - pub unsafe fn set_description(&mut self, path: &str) -> Result<()> { - let desc = string_to_u16(path); - let desc = PCWSTR(desc.as_ptr()); - Ok(self.me.SetDescription(desc)?) + pub unsafe fn set_description>(&mut self, path: P) -> Result<()> { + let desc = string_to_wide(path.as_ref()); + Ok(self.me.SetDescription(desc.as_pcwstr())?) } - pub unsafe fn set_icon_location(&mut self, path: &str, index: i32) -> Result<()> { - let icon = string_to_u16(path); - let icon = PCWSTR(icon.as_ptr()); - Ok(self.me.SetIconLocation(icon, index)?) + pub unsafe fn set_icon_location>(&mut self, path: P, index: i32) -> Result<()> { + let icon = string_to_wide(path.as_ref()); + Ok(self.me.SetIconLocation(icon.as_pcwstr(), index)?) } - pub unsafe fn set_target_path(&mut self, path: &str) -> Result<()> { - let target = string_to_u16(path); - let target = PCWSTR(target.as_ptr()); - Ok(self.me.SetPath(target)?) + pub unsafe fn set_target_path>(&mut self, path: P) -> Result<()> { + let target = string_to_wide(path.as_ref()); + Ok(self.me.SetPath(target.as_pcwstr())?) } - pub unsafe fn set_working_directory(&mut self, path: &str) -> Result<()> { - let work_dir = string_to_u16(path); - let work_dir = PCWSTR(work_dir.as_ptr()); - Ok(self.me.SetWorkingDirectory(work_dir)?) + pub unsafe fn set_working_directory>(&mut self, path: P) -> Result<()> { + let work_dir = string_to_wide(path.as_ref()); + Ok(self.me.SetWorkingDirectory(work_dir.as_pcwstr())?) } - pub unsafe fn set_aumid(&mut self, app_model_id: Option<&str>) -> Result<()> { + pub unsafe fn set_aumid>(&mut self, app_model_id: Option

) -> Result<()> { // Set app user model ID property // Docs: https://docs.microsoft.com/windows/win32/properties/props-system-appusermodel-id let store: IPropertyStore = self.me.cast()?; if let Some(app_model_id) = app_model_id { - let id = string_to_u16(app_model_id); - let id = PCWSTR(id.as_ptr()); - let variant = InitPropVariantFromStringVector(Some(&[id]))?; + let id = string_to_wide(app_model_id.as_ref()); + let variant = InitPropVariantFromStringVector(Some(&[id.as_pcwstr()]))?; store.SetValue(&PKEY_AppUserModel_ID, &variant)?; } else { let prop_variant = PROPVARIANT::default(); // defaults to VT_EMPTY @@ -515,26 +517,26 @@ impl Lnk { } pub unsafe fn save(&mut self) -> Result<()> { + if self.my_path.as_os_str().is_empty() { + return Err(anyhow!("Cannot save shortcut without a path.")); + } Ok(self.pf.Save(None, true)?) } - pub unsafe fn save_as(&mut self, path: &str) -> Result<()> { - let output = string_to_u16(path); - let output = PCWSTR(output.as_ptr()); - self.my_path = path.to_string(); - Ok(self.pf.Save(output, true)?) + pub unsafe fn save_as>(&mut self, path: P) -> Result<()> { + let output = string_to_wide(path.as_ref()); + self.my_path = path.as_ref().to_path_buf(); + Ok(self.pf.Save(output.as_pcwstr(), true)?) } pub unsafe fn open_write>(link_path: P) -> Result { - let link_path = link_path.as_ref().to_string_lossy().to_string(); + let link_path = link_path.as_ref(); let link: IShellLinkW = create_instance(&ShellLink)?; let persist: IPersistFile = link.cast()?; - debug!("Loading link: {}", link_path); + debug!("Loading link: {:?}", link_path); - let link_pcwstr = string_to_u16(&link_path); - let link_pcwstr = PCWSTR(link_pcwstr.as_ptr()); - - persist.Load(link_pcwstr, STGM_READWRITE)?; + let link_pcwstr = string_to_wide(link_path); + persist.Load(link_pcwstr.as_pcwstr(), STGM_READWRITE)?; // we don't really want to "resolve" the shortcut in the middle of an update operation // this can cause Windows to move the target path of a shortcut to one of our temp dirs etc @@ -548,19 +550,19 @@ impl Lnk { // warn!("Failed to resolve link {} ({:?})", link_path, e); // } - Ok(Lnk { me: link, pf: persist, my_path: link_path }) + Ok(Lnk { me: link, pf: persist, my_path: link_path.to_path_buf() }) } pub unsafe fn create_new() -> Result { let link: IShellLinkW = create_instance(&ShellLink)?; let persist: IPersistFile = link.cast()?; - Ok(Lnk { me: link, pf: persist, my_path: String::new() }) + Ok(Lnk { me: link, pf: persist, my_path: PathBuf::new() }) } } impl std::fmt::Debug for Lnk { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Lnk({})", self.my_path) + write!(f, "Lnk({:?})", self.my_path) } } @@ -626,7 +628,7 @@ fn test_can_resolve_existing_shortcut() { unsafe { unsafe_run_delegate_in_com_context(move || { let l = Lnk::open_write(link_path).unwrap(); - let target = l.get_target_path().unwrap(); + let target = l.get_target_path().unwrap().to_string_lossy().to_string(); assert_eq!(target, "C:\\Users\\Caelan\\AppData\\Local\\Discord\\Update.exe"); Ok(()) }) @@ -696,8 +698,8 @@ fn test_shortcut_full_integration() { assert_eq!(shortcuts[2].0, ShortcutLocationFlags::START_MENU); assert_eq!(PathBuf::from(&shortcuts[2].1.my_path), link2); - assert_eq!(shortcuts[0].1.get_target_path().unwrap(), target); - assert_eq!(shortcuts[0].1.get_working_directory().unwrap(), work); + assert_eq!(shortcuts[0].1.get_target_path().unwrap().to_string_lossy().to_string(), target); + assert_eq!(shortcuts[0].1.get_working_directory().unwrap().to_string_lossy().to_string(), work); unsafe_remove_all_shortcuts_for_root_dir(root); assert!(!link1.exists()); diff --git a/src/bins/src/windows/splash.rs b/src/bins/src/windows/splash.rs index b7e679fa..2ffaf054 100644 --- a/src/bins/src/windows/splash.rs +++ b/src/bins/src/windows/splash.rs @@ -1,8 +1,8 @@ -use super::strings::string_to_wide; use anyhow::{bail, Result}; use image::{codecs::gif::GifDecoder, AnimationDecoder, DynamicImage, ImageFormat, ImageReader}; use std::sync::mpsc::{self, Receiver, Sender}; use std::{io::Cursor, thread}; +use velopack::wide_strings::string_to_wide; use windows::{ core::HRESULT, Win32::{ diff --git a/src/bins/src/windows/strings.rs b/src/bins/src/windows/strings.rs deleted file mode 100644 index f56fe1d9..00000000 --- a/src/bins/src/windows/strings.rs +++ /dev/null @@ -1,149 +0,0 @@ -use anyhow::Result; -use windows::core::{PCWSTR, PWSTR}; - -pub struct WideString { - inner: Vec, -} - -impl WideString { - pub fn as_ptr(&self) -> *const u16 { - self.inner.as_ptr() - } - - pub fn as_mut_ptr(&mut self) -> *mut u16 { - self.inner.as_mut_ptr() - } - - pub fn len(&self) -> usize { - self.inner.len() - } - - pub fn as_pcwstr(&self) -> PCWSTR { - PCWSTR(self.as_ptr()) - } - - pub fn as_pwstr(&mut self) -> PWSTR { - PWSTR(self.as_mut_ptr()) - } -} - -impl Into> for WideString { - fn into(self) -> Vec { - self.inner - } -} - -impl Into for WideString { - fn into(self) -> PCWSTR { - self.as_pcwstr() - } -} - -impl AsRef<[u16]> for WideString { - fn as_ref(&self) -> &[u16] { - &self.inner - } -} - -impl AsMut<[u16]> for WideString { - fn as_mut(&mut self) -> &mut [u16] { - &mut self.inner - } -} - -pub fn string_to_u16>(input: P) -> Vec { - let input = input.as_ref(); - input.encode_utf16().chain(Some(0)).collect::>() -} - -pub fn string_to_wide>(input: P) -> WideString { - WideString { inner: string_to_u16(input) } -} - -pub trait WideStringRef { - fn to_wide_slice(&self) -> &[u16]; -} - -impl WideStringRef for PWSTR { - fn to_wide_slice(&self) -> &[u16] { - unsafe { self.as_wide() } - } -} - -impl WideStringRef for PCWSTR { - fn to_wide_slice(&self) -> &[u16] { - unsafe { self.as_wide() } - } -} - -impl WideStringRef for Vec { - fn to_wide_slice(&self) -> &[u16] { - self.as_ref() - } -} - -impl WideStringRef for &Vec { - fn to_wide_slice(&self) -> &[u16] { - self.as_ref() - } -} - -// impl WideString for [u16] { -// fn to_wide_slice(&self) -> &[u16] { -// self.as_ref() -// } -// } - -impl WideStringRef for [u16; N] { - fn to_wide_slice(&self) -> &[u16] { - self.as_ref() - } -} - -pub fn u16_to_string_lossy(input: T) -> String { - let slice = input.to_wide_slice(); - let null_pos = slice.iter().position(|&x| x == 0).unwrap_or(slice.len()); - let trimmed_slice = &slice[..null_pos]; - String::from_utf16_lossy(trimmed_slice) -} - -pub fn u16_to_string(input: T) -> Result { - let slice = input.to_wide_slice(); - let null_pos = slice.iter().position(|&x| x == 0).unwrap_or(slice.len()); - let trimmed_slice = &slice[..null_pos]; - Ok(String::from_utf16(trimmed_slice)?) -} - - -// pub fn pwstr_to_string(input: PWSTR) -> Result { -// unsafe { -// let hstring = input.to_hstring(); -// let string = hstring.to_string_lossy(); -// Ok(string.trim_end_matches('\0').to_string()) -// } -// } - -// pub fn pcwstr_to_string(input: PCWSTR) -> Result { -// unsafe { -// let hstring = input.to_hstring(); -// let string = hstring.to_string_lossy(); -// Ok(string.trim_end_matches('\0').to_string()) -// } -// } - -// pub fn u16_to_string>(input: T) -> Result { -// let input = input.as_ref(); -// let hstring = HSTRING::from_wide(input); -// let string = hstring.to_string_lossy(); -// Ok(string.trim_end_matches('\0').to_string()) -// } - -// pub fn u16_to_string>(input: T) -> Result { -// let input = input.as_ref(); -// // Find position of first null byte (0) -// let null_pos = input.iter().position(|&x| x == 0).unwrap_or(input.len()); -// // Take only up to the first null byte -// let trimmed_input = &input[..null_pos]; -// let hstring = HSTRING::from_wide(trimmed_input); -// Ok(hstring.to_string_lossy()) -// } diff --git a/src/bins/src/windows/util.rs b/src/bins/src/windows/util.rs index d6bd680e..02eb4d3f 100644 --- a/src/bins/src/windows/util.rs +++ b/src/bins/src/windows/util.rs @@ -1,16 +1,17 @@ -use crate::{ - shared::{self, runtime_arch::RuntimeArch}, - windows::strings::{string_to_u16, u16_to_string}, -}; +use crate::shared::{self, runtime_arch::RuntimeArch}; use anyhow::{anyhow, Result}; use normpath::PathExt; use std::{ + ffi::{OsStr, OsString}, path::{Path, PathBuf}, time::Duration, }; -use velopack::{locator::VelopackLocator, process, process::WaitResult}; +use velopack::{ + locator::VelopackLocator, + process::{self, WaitResult}, + wide_strings::{string_to_wide, wide_to_os_string}, +}; use windows::{ - core::PCWSTR, Win32::Storage::FileSystem::GetLongPathNameW, Win32::System::SystemInformation::{VerSetConditionMask, VerifyVersionInfoW, OSVERSIONINFOEXW, VER_FLAGS}, }; @@ -21,17 +22,11 @@ pub fn run_hook(locator: &VelopackLocator, hook_name: &str, timeout_secs: u64) - let current_path = locator.get_current_bin_dir(); let main_exe_path = locator.get_main_exe_path(); let ver_string = locator.get_manifest_version_full_string(); - let args = vec![hook_name, &ver_string]; + let args: Vec = vec![hook_name.into(), ver_string.into()]; let mut success = false; info!("Running {} hook...", hook_name); - let cmd = process::run_process( - main_exe_path, - args.iter().map(|f| f.to_string()).collect(), - Some(current_path.to_string_lossy().to_string()), - false, - None, - ); + let cmd = process::run_process(main_exe_path, args, Some(current_path), false, None); if let Err(e) = cmd { warn!("Failed to start hook {}: {}", hook_name, e); @@ -66,22 +61,22 @@ pub fn run_hook(locator: &VelopackLocator, hook_name: &str, timeout_secs: u64) - success } -pub fn expand_environment_strings>(input: P) -> Result { +pub fn expand_environment_strings>(input: P) -> Result { use windows::Win32::System::Environment::ExpandEnvironmentStringsW; - let encoded_u16 = super::strings::string_to_u16(input); - let encoded = PCWSTR(encoded_u16.as_ptr()); - let mut buffer_size = unsafe { ExpandEnvironmentStringsW(encoded, None) }; + let input = input.as_ref(); + let encoded = string_to_wide(input); + let mut buffer_size = unsafe { ExpandEnvironmentStringsW(encoded.as_pcwstr(), None) }; if buffer_size == 0 { return Err(anyhow!(windows::core::Error::from_win32())); } let mut buffer: Vec = vec![0; buffer_size as usize]; - buffer_size = unsafe { ExpandEnvironmentStringsW(encoded, Some(&mut buffer)) }; + buffer_size = unsafe { ExpandEnvironmentStringsW(encoded.as_pcwstr(), Some(&mut buffer)) }; if buffer_size == 0 { return Err(anyhow!(windows::core::Error::from_win32())); } - super::strings::u16_to_string(buffer) + Ok(wide_to_os_string(buffer)) } #[test] @@ -91,24 +86,23 @@ fn test_expand_environment_strings() { assert!(expand_environment_strings("%windir%\\system32\\").unwrap().eq_ignore_ascii_case("C:\\Windows\\system32\\")); } -pub fn get_long_path>(str: P) -> Result { - let str = str.as_ref().to_string(); - let str = string_to_u16(str); - let str = PCWSTR(str.as_ptr()); +pub fn get_long_path>(str: P) -> Result { + let str = str.as_ref(); + let str = string_to_wide(str); + // SAFETY: str is a valid wide string, this call will return required size of buffer - let len = unsafe { GetLongPathNameW(str, None) }; + let len = unsafe { GetLongPathNameW(str.as_pcwstr(), None) }; if len == 0 { return Err(anyhow!(windows::core::Error::from_win32())); } let mut vec = vec![0u16; len as usize]; - let len = unsafe { GetLongPathNameW(str, Some(vec.as_mut_slice())) }; + let len = unsafe { GetLongPathNameW(str.as_pcwstr(), Some(vec.as_mut_slice())) }; if len == 0 { return Err(anyhow!(windows::core::Error::from_win32())); } - let result = u16_to_string(vec)?; - Ok(result) + Ok(wide_to_os_string(vec)) } pub fn is_directory_writable>(path: P1) -> bool { @@ -165,21 +159,12 @@ pub fn is_sub_path, P2: AsRef>(path: P1, parent: P2) -> Re let path = path.normalize().or_else(|_| path.normalize_virtually())?; let parent = parent.normalize().or_else(|_| parent.normalize_virtually())?; - let mut path = path.as_path().to_string_lossy().to_string(); - let mut parent = parent.as_path().to_string_lossy().to_string(); - // calls GetLongPathNameW - match get_long_path(&path) { - Ok(p) => path = p, - Err(e) => warn!("Failed to get long path for '{}': {}", path, e), - } - match get_long_path(&parent) { - Ok(p) => parent = p, - Err(e) => warn!("Failed to get long path for '{}': {}", parent, e), - } + let mut path = get_long_path(&path).unwrap_or(path.into()); + let mut parent = get_long_path(&parent).unwrap_or(parent.into()); - path = path.to_lowercase(); - parent = parent.to_lowercase(); + path = path.to_ascii_lowercase(); + parent = parent.to_ascii_lowercase(); let path = PathBuf::from(path); let parent = PathBuf::from(parent); @@ -309,7 +294,11 @@ pub fn is_os_version_or_greater(version: &str) -> Result { } if major == 8 { - return Ok(if minor >= 1 { is_windows_8_or_greater() } else { is_windows_8_1_or_greater() }); + return Ok(if minor >= 1 { + is_windows_8_or_greater() + } else { + is_windows_8_1_or_greater() + }); } // https://en.wikipedia.org/wiki/List_of_Microsoft_Windows_versions diff --git a/src/lib-cpp/src/csource.rs b/src/lib-cpp/src/csource.rs index 9a2d051d..66e9e940 100644 --- a/src/lib-cpp/src/csource.rs +++ b/src/lib-cpp/src/csource.rs @@ -4,6 +4,7 @@ use libc::{c_void, size_t}; use std::{ collections::HashMap, ffi::CString, + path::Path, sync::{ atomic::{AtomicUsize, Ordering}, mpsc::Sender, @@ -43,7 +44,7 @@ impl UpdateSource for CCallbackUpdateSource { if let Some(cb_get_release_feed) = self.cb_get_release_feed { let json_cstr_ptr = (cb_get_release_feed)(self.p_user_data, releases_name_cstr.as_ptr()); let json = c_to_String(json_cstr_ptr).map_err(|_| { - Error::Generic("User vpkc_release_feed_delegate_t returned a null pointer instead of an asset feed".to_string()) + Error::Other("User vpkc_release_feed_delegate_t returned a null pointer instead of an asset feed".to_string()) })?; if let Some(cb_free_release_feed) = self.cb_free_release_feed { (cb_free_release_feed)(self.p_user_data, json_cstr_ptr); // Free the C string returned by the callback @@ -53,12 +54,13 @@ impl UpdateSource for CCallbackUpdateSource { let feed: VelopackAssetFeed = serde_json::from_str(&json)?; Ok(feed) } else { - Err(Error::Generic("User vpkc_release_feed_delegate_t is null".to_string())) + Err(Error::Other("User vpkc_release_feed_delegate_t is null".to_string())) } } - fn download_release_entry(&self, asset: &VelopackAsset, local_file: &str, progress_sender: Option>) -> Result<(), Error> { + fn download_release_entry(&self, asset: &VelopackAsset, local_file: &Path, progress_sender: Option>) -> Result<(), Error> { if let Some(cb_download_release_entry) = self.cb_download_release_entry { + let local_file = local_file.to_string_lossy().to_string(); let local_file_cstr = CString::new(local_file).unwrap(); let asset_ptr = unsafe { allocate_VelopackAsset(asset) }; @@ -76,12 +78,12 @@ impl UpdateSource for CCallbackUpdateSource { } if !success { - return Err(Error::Generic("User vpkc_download_asset_delegate_t returned false to indicate download failed".to_owned())); + return Err(Error::Other("User vpkc_download_asset_delegate_t returned false to indicate download failed".to_owned())); } Ok(()) } else { - Err(Error::Generic("User vpkc_download_asset_delegate_t is null".to_string())) + Err(Error::Other("User vpkc_download_asset_delegate_t is null".to_string())) } } diff --git a/src/lib-cpp/src/types.rs b/src/lib-cpp/src/types.rs index 48d45c9e..689455e9 100644 --- a/src/lib-cpp/src/types.rs +++ b/src/lib-cpp/src/types.rs @@ -1,8 +1,8 @@ use anyhow::{bail, Result}; use libc::{c_char, c_void, size_t}; use std::ffi::{CStr, CString}; -use std::path::PathBuf; use std::mem::size_of; +use std::path::PathBuf; use velopack::{locator::VelopackLocatorConfig, UpdateInfo, UpdateOptions, VelopackAsset}; /// The result of a call to check for updates. This can indicate that an update is available, or that an error occurred. diff --git a/src/lib-rust/src/bundle.rs b/src/lib-rust/src/bundle.rs index f9f357e1..bde61103 100644 --- a/src/lib-rust/src/bundle.rs +++ b/src/lib-rust/src/bundle.rs @@ -1,5 +1,6 @@ #![allow(missing_docs)] +use std::io::Cursor; use std::{ cell::RefCell, fs::{self, File}, @@ -7,15 +8,14 @@ use std::{ path::{Path, PathBuf}, rc::Rc, }; -use std::io::Cursor; use regex::Regex; use semver::Version; -use xml::EventReader; use xml::reader::XmlEvent; +use xml::EventReader; use zip::ZipArchive; -use crate::{Error, misc}; +use crate::{misc, Error}; #[cfg(target_os = "macos")] use std::os::unix::fs::PermissionsExt; @@ -38,14 +38,14 @@ pub struct BundleZip<'a> { pub fn load_bundle_from_file<'a, P: AsRef>(file_name: P) -> Result, Error> { let file_name = file_name.as_ref(); - debug!("Loading bundle from file '{}'...", file_name.to_string_lossy()); + debug!("Loading bundle from file '{:?}'...", file_name); let file = misc::retry_io(|| File::open(&file_name))?; let cursor: Box = Box::new(file); let zip = ZipArchive::new(cursor)?; - Ok(BundleZip { - zip: Rc::new(RefCell::new(zip)), - zip_from_file: true, - file_path: Some(file_name.to_owned()), + Ok(BundleZip { + zip: Rc::new(RefCell::new(zip)), + zip_from_file: true, + file_path: Some(file_name.to_owned()), zip_range: None, manifest: None, }) @@ -55,13 +55,7 @@ pub fn load_bundle_from_memory(zip_range: &[u8]) -> Result { info!("Loading bundle from embedded zip..."); let cursor: Box = Box::new(Cursor::new(zip_range)); let zip = ZipArchive::new(cursor)?; - Ok(BundleZip { - zip: Rc::new(RefCell::new(zip)), - zip_from_file: false, - zip_range: Some(zip_range), - file_path: None, - manifest: None, - }) + Ok(BundleZip { zip: Rc::new(RefCell::new(zip)), zip_from_file: false, zip_range: Some(zip_range), file_path: None, manifest: None }) } #[allow(dead_code)] @@ -75,7 +69,7 @@ impl BundleZip<'_> { } Ok(()) } - + pub fn calculate_size(&self) -> (u64, u64) { let mut total_uncompressed_size = 0u64; let mut total_compressed_size = 0u64; @@ -140,7 +134,7 @@ impl BundleZip<'_> { pub fn extract_zip_idx_to_path>(&self, index: usize, path: T) -> Result<(), Error> { let path = path.as_ref(); - debug!("Extracting zip file to path: {}", path.to_string_lossy()); + debug!("Extracting zip file to path: {:?}", path); let p = PathBuf::from(path); let parent = p.parent().unwrap(); @@ -172,7 +166,7 @@ impl BundleZip<'_> { { let idx = self.find_zip_file(predicate); if idx.is_none() { - return Err(Error::FileNotFound("(zip bundle predicate)".to_owned())); + return Err(Error::InvalidPackage("(zip bundle predicate not found)".to_owned())); } let idx = idx.unwrap(); self.extract_zip_idx_to_path(idx, path)?; @@ -183,15 +177,15 @@ impl BundleZip<'_> { if let Some(manifest) = &self.manifest { return Ok(manifest.clone()); } - - let nuspec_idx = self.find_zip_file(|name| name.ends_with(".nuspec")) - .ok_or(Error::MissingNuspec)?; + + let nuspec_idx = + self.find_zip_file(|name| name.ends_with(".nuspec")).ok_or(Error::InvalidPackage("No .nuspec manifest found".into()))?; let mut contents = String::new(); let mut archive = self.zip.borrow_mut(); archive.by_index(nuspec_idx)?.read_to_string(&mut contents)?; let app = read_manifest_from_string(&contents)?; - + self.manifest = Some(app.clone()); Ok(app) } @@ -218,19 +212,23 @@ impl BundleZip<'_> { { let absolute_path = link_path.parent().unwrap().join(&target_path); trace!( - "Creating symlink '{}' -> '{}', target isfile={}, isdir={}, relative={}", - link_path.to_string_lossy(), - absolute_path.to_string_lossy(), + "Creating symlink '{:?}' -> '{:?}', target isfile={}, isdir={}, relative={:?}", + link_path, + absolute_path, absolute_path.is_file(), absolute_path.is_dir(), - target_path.to_string_lossy() + target_path ); if absolute_path.is_file() { std::os::windows::fs::symlink_file(target_path, link_path)?; } else if absolute_path.is_dir() { std::os::windows::fs::symlink_dir(target_path, link_path)?; } else { - return Err(Error::Generic("Could not create symlink: target is not a file or directory.".to_owned())); + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("Target path '{:?}' does not exist or is of an invalid type", absolute_path), + ) + .into()); } } #[cfg(not(target_os = "windows"))] @@ -246,7 +244,7 @@ impl BundleZip<'_> { let files = self.get_file_names()?; let num_files = files.len(); - info!("Extracting {} app files to '{}'...", num_files, current_path.to_string_lossy()); + info!("Extracting {} app files to '{:?}'...", num_files, current_path); let re = Regex::new(r"lib[\\\/][^\\\/]*[\\\/]").unwrap(); let stub_regex = Regex::new("_ExecutionStub.exe$").unwrap(); let symlink_regex = Regex::new(".__symlink$").unwrap(); @@ -259,7 +257,7 @@ impl BundleZip<'_> { let nuspec_path = current_path.join("sq.version"); let _ = self .extract_zip_predicate_to_path(|name| name.ends_with(".nuspec"), nuspec_path) - .map_err(|_| Error::MissingNuspec)?; + .map_err(|_| Error::InvalidPackage("No .nuspec manifest found".into()))?; } // we extract the symlinks after, because the target must exist. @@ -294,16 +292,16 @@ impl BundleZip<'_> { #[cfg(target_os = "windows")] let file_path_on_disk = file_path_on_disk.as_path(); - debug!(" {} Extracting '{}' to '{}'", i, key, file_path_on_disk.to_string_lossy()); + debug!(" {} Extracting '{}' to '{:?}'", i, key, file_path_on_disk); self.extract_zip_idx_to_path(i, &file_path_on_disk)?; // on macos, we need to chmod +x the executable files - // for now, we just chmod 755 every file we extract. this is not great, ideally we + // for now, we just chmod 755 every file we extract. this is not great, ideally we // will preserve the mode as it was when packaging. This will come in a future release. #[cfg(target_os = "macos")] { if let Err(e) = std::fs::set_permissions(&file_path_on_disk, std::fs::Permissions::from_mode(0o755)) { - warn!("Failed to set mode 755 on '{}': {}", file_path_on_disk.to_string_lossy(), e); + warn!("Failed to set mode 755 on '{}': {:?}", file_path_on_disk, e); } } @@ -316,7 +314,7 @@ impl BundleZip<'_> { let mut file = archive.by_index(i)?; let mut contents = String::new(); file.read_to_string(&mut contents)?; - info!(" {} Creating symlink '{}' -> '{}'", i, link_path.to_string_lossy(), contents); + info!(" {} Creating symlink '{:?}' -> '{}'", i, link_path, contents); let contents = contents.trim_end_matches('/'); #[cfg(target_os = "windows")] @@ -418,16 +416,16 @@ pub fn read_manifest_from_string(xml: &str) -> Result { } if obj.id.is_empty() { - return Err(Error::MissingNuspecProperty("id".to_owned())); + return Err(Error::InvalidPackage("Missing required manifest property: id".into())); } if obj.version == Version::new(0, 0, 0) { - return Err(Error::MissingNuspecProperty("version".to_owned())); + return Err(Error::InvalidPackage("Missing required manifest property: version".into())); } #[cfg(target_os = "windows")] if obj.main_exe.is_empty() { - return Err(Error::MissingNuspecProperty("mainExe".to_owned())); + return Err(Error::InvalidPackage("Missing required manifest property: mainExe".into())); } if obj.title.is_empty() { @@ -485,7 +483,11 @@ fn parse_package_file_name>(name: T) -> Option { let mut entry = EntryNameInfo::default(); entry.is_delta = delta; - let name_and_ver = if full { ENTRY_SUFFIX_FULL.replace(name, "") } else { ENTRY_SUFFIX_DELTA.replace(name, "") }; + let name_and_ver = if full { + ENTRY_SUFFIX_FULL.replace(name, "") + } else { + ENTRY_SUFFIX_DELTA.replace(name, "") + }; let ver_idx = ENTRY_VERSION_START.find(&name_and_ver); if ver_idx.is_none() { return None; @@ -528,4 +530,4 @@ fn test_parse_package_file_name() { assert!(parse_package_file_name("MyCoolApp-1.2.3-beta1-win7-x64-full.zip").is_none()); assert!(parse_package_file_name("MyCoolApp-1.2.3.nupkg").is_none()); assert!(parse_package_file_name("MyCoolApp-1.2-full.nupkg").is_none()); -} \ No newline at end of file +} diff --git a/src/lib-rust/src/download.rs b/src/lib-rust/src/download.rs index c678c176..2aedb56c 100644 --- a/src/lib-rust/src/download.rs +++ b/src/lib-rust/src/download.rs @@ -1,10 +1,11 @@ use std::fs::File; use std::io::{Read, Write}; +use std::path::Path; use crate::{misc, Error}; /// Downloads a file from a URL and writes it to a file while reporting progress from 0-100. -pub fn download_url_to_file(url: &str, file_path: &str, mut progress: A) -> Result<(), Error> +pub fn download_url_to_file(url: &str, file_path: &Path, mut progress: A) -> Result<(), Error> where A: FnMut(i16), { @@ -74,7 +75,7 @@ fn test_download_file_reports_progress() { let tmpfile = tempfile::NamedTempFile::new().unwrap(); let tmppath = tmpfile.path(); - download_url_to_file(test_file, tmppath.to_str().unwrap(), |p| { + download_url_to_file(test_file, Path::new(tmppath), |p| { assert!(p >= last_prog); prog_count += 1; last_prog = p; @@ -117,11 +118,7 @@ fn test_interrupted_download() { }); let tmpfile = tempfile::NamedTempFile::new().unwrap(); - let result = download_url_to_file( - &format!("http://{}", addr), - tmpfile.path().to_str().unwrap(), - |_| {} - ); + let result = download_url_to_file(&format!("http://{}", addr), tmpfile.path(), |_| {}); assert!(result.is_err(), "Download should fail due to connection interruption"); } @@ -152,13 +149,9 @@ fn test_successful_download() { }); let tmpfile = tempfile::NamedTempFile::new().unwrap(); - let _ = download_url_to_file( - &format!("http://{}", addr), - tmpfile.path().to_str().unwrap(), - |_| {}, - ).unwrap(); + let _ = download_url_to_file(&format!("http://{}", addr), tmpfile.path(), |_| {}).unwrap(); // Verify that the downloaded file has the expected size let metadata = tmpfile.path().metadata().unwrap(); assert_eq!(metadata.len(), 10240, "Downloaded file size should match the expected content size"); -} \ No newline at end of file +} diff --git a/src/lib-rust/src/lib.rs b/src/lib-rust/src/lib.rs index e2bbc13a..2d6ae55d 100644 --- a/src/lib-rust/src/lib.rs +++ b/src/lib-rust/src/lib.rs @@ -116,6 +116,8 @@ macro_rules! maybe_pub_os { }; } +use std::path::PathBuf; + mod app; pub use app::*; @@ -128,6 +130,7 @@ pub mod locator; /// Sources are abstractions for custom update sources (eg. url, local file, github releases, etc). pub mod sources; +maybe_pub!(wide_strings); maybe_pub!(download, bundle, constants, lockfile, logging, misc); maybe_pub_os!(process, "process_win.rs", "process_unix.rs"); @@ -147,13 +150,13 @@ pub enum NetworkError { #[allow(missing_docs)] pub enum Error { #[error("File does not exist: {0}")] - FileNotFound(String), + FileNotFound(PathBuf), #[error("IO error: {0}")] Io(#[from] std::io::Error), #[error("Checksum did not match for {0} (expected {1}, actual {2})")] - ChecksumInvalid(String, String, String), + ChecksumInvalid(PathBuf, String, String), #[error("Size did not match for {0} (expected {1}, actual {2})")] - SizeInvalid(String, u64, u64), + SizeInvalid(PathBuf, u64, u64), #[error("Zip error: {0}")] Zip(#[from] zip::result::ZipError), #[error("Network error: {0}")] @@ -162,16 +165,14 @@ pub enum Error { Json(#[from] serde_json::Error), #[error("Semver parse error: {0}")] Semver(#[from] semver::Error), - #[error("This application is missing a package manifest (.nuspec) or it could not be parsed.")] - MissingNuspec, - #[error("This application is missing a required property in its package manifest: {0}")] - MissingNuspecProperty(String), - #[error("This application is missing an Update.exe/UpdateNix/UpdateMac binary.")] - MissingUpdateExe, + #[error("This update package is invalid: {0}.")] + InvalidPackage(String), #[error("This application is not properly installed: {0}")] NotInstalled(String), - #[error("Generic error: {0}")] - Generic(String), + #[error("This is not supported: {0}")] + NotSupported(String), + #[error("{0}")] + Other(String), #[cfg(target_os = "windows")] #[error("Win32 error: {0}")] Win32(#[from] windows::core::Error), diff --git a/src/lib-rust/src/locator.rs b/src/lib-rust/src/locator.rs index 22089d98..fae3ff5d 100644 --- a/src/lib-rust/src/locator.rs +++ b/src/lib-rust/src/locator.rs @@ -110,10 +110,10 @@ impl VelopackLocator { /// Creates a new VelopackLocator from the given paths, trying to auto-detect the manifest. pub fn new(config: &VelopackLocatorConfig) -> Result { if !config.UpdateExePath.exists() { - return Err(Error::MissingUpdateExe); + return Err(Error::NotInstalled("Update.exe does not exist in the expected path".to_owned())); } if !config.ManifestPath.exists() { - return Err(Error::MissingNuspec); + return Err(Error::NotInstalled("Manifest file does not exist in the expected path".to_owned())); } let manifest = read_current_manifest(&config.ManifestPath)?; @@ -152,11 +152,6 @@ impl VelopackLocator { self.paths.PackagesDir.clone() } - /// Returns the path to the current app's packages directory as a string. - pub fn get_packages_dir_as_string(&self) -> String { - Self::path_as_string(&self.paths.PackagesDir) - } - /// Returns the path to the ideal local nupkg path. pub fn get_ideal_local_nupkg_path(&self, id: Option<&str>, version: Option) -> PathBuf { let id = id.unwrap_or(&self.manifest.id); @@ -164,11 +159,6 @@ impl VelopackLocator { self.paths.RootAppDir.join("packages").join(format!("{}-{}-full.nupkg", id, version)) } - /// Returns the path to the ideal local nupkg path as a string. - pub fn get_ideal_local_nupkg_path_as_string(&self, id: Option<&str>, version: Option) -> String { - Self::path_as_string(&self.get_ideal_local_nupkg_path(id, version)) - } - /// Returns the path to the current app temporary directory. pub fn get_temp_dir_root(&self) -> PathBuf { self.paths.PackagesDir.join("VelopackTemp") @@ -179,51 +169,26 @@ impl VelopackLocator { self.get_temp_dir_root().join("tmp_".to_string() + &misc::random_string(16)) } - /// Returns the path to the current app temporary directory as a string. - pub fn get_temp_dir_as_string(&self) -> String { - Self::path_as_string(&self.get_temp_dir_root()) - } - /// Returns the root directory of the current app. pub fn get_root_dir(&self) -> PathBuf { self.paths.RootAppDir.clone() } - /// Returns the root directory of the current app as a string. - pub fn get_root_dir_as_string(&self) -> String { - Self::path_as_string(&self.paths.RootAppDir) - } - /// Returns the path to the current app's Update.exe binary. pub fn get_update_path(&self) -> PathBuf { self.paths.UpdateExePath.clone() } - /// Returns the path to the current app's Update.exe binary as a string. - pub fn get_update_path_as_string(&self) -> String { - Self::path_as_string(&self.paths.UpdateExePath) - } - /// Returns the path to the current app's main executable. pub fn get_main_exe_path(&self) -> PathBuf { self.paths.CurrentBinaryDir.join(&self.manifest.main_exe) } - /// Returns the path to the current app's main executable as a string. - pub fn get_main_exe_path_as_string(&self) -> String { - Self::path_as_string(&self.get_main_exe_path()) - } - /// Returns the path to the current app's user binary directory. pub fn get_current_bin_dir(&self) -> PathBuf { self.paths.CurrentBinaryDir.clone() } - /// Returns the path to the current app's user binary directory as a string. - pub fn get_current_bin_dir_as_string(&self) -> String { - Self::path_as_string(&self.paths.CurrentBinaryDir) - } - /// Returns a clone of the current app's manifest. pub fn get_manifest(&self) -> Manifest { self.manifest.clone() @@ -311,10 +276,6 @@ impl VelopackLocator { Ok(lock_file) } - fn path_as_string(path: &PathBuf) -> String { - path.to_string_lossy().to_string() - } - fn get_or_create_staged_user_id(&self) -> String { let packages_dir = self.get_packages_dir(); let beta_id_path = packages_dir.join(".betaId"); @@ -535,24 +496,24 @@ fn read_current_manifest(nuspec_path: &PathBuf) -> Result { return bundle::read_manifest_from_string(&nuspec); } } - Err(Error::MissingNuspec) + Err(Error::NotInstalled(format!("Manifest file does not exist or is not readable: {:?}", nuspec_path))) } /// Returns the path and manifest of the latest full package in the given directory. pub fn find_latest_full_package(packages_dir: &PathBuf) -> Option<(PathBuf, Manifest)> { let packages_dir = packages_dir.to_string_lossy(); - info!("Attempting to auto-detect package in: {}", packages_dir); + info!("Attempting to auto-detect package in: {:?}", packages_dir); let mut package: Option<(PathBuf, Manifest)> = None; let search_glob = format!("{}/*-full.nupkg", packages_dir); if let Ok(paths) = glob::glob(search_glob.as_str()) { for path in paths.into_iter().flatten() { - trace!("Checking package: '{}'", path.to_string_lossy()); + trace!("Checking package: '{:?}'", path); if let Ok(mut bun) = bundle::load_bundle_from_file(&path) { if let Ok(mani) = bun.read_manifest() { if package.is_none() || mani.version > package.clone()?.1.version { - info!("Found {}: '{}'", mani.version, path.to_string_lossy()); + info!("Found {}: '{:?}'", mani.version, path); package = Some((path, mani)); } } diff --git a/src/lib-rust/src/manager.rs b/src/lib-rust/src/manager.rs index 5c1c3bf4..ad97d0b7 100644 --- a/src/lib-rust/src/manager.rs +++ b/src/lib-rust/src/manager.rs @@ -1,6 +1,7 @@ use semver::Version; use serde::{Deserialize, Serialize}; -use std::path::PathBuf; +use std::ffi::{OsStr, OsString}; +use std::path::{Path, PathBuf}; use std::{fs, process::exit, sync::mpsc::Sender}; #[cfg(feature = "async")] @@ -396,7 +397,7 @@ impl UpdateManager { let partial_file = final_target_file.with_extension("partial"); if final_target_file.exists() { - info!("Package already exists on disk, skipping download: '{}'", final_target_file.to_string_lossy()); + info!("Package already exists on disk, skipping download: '{:?}'", final_target_file); return Ok(()); } @@ -405,11 +406,11 @@ impl UpdateManager { let delta_pattern = format!("{}/*-delta.nupkg", packages_dir.to_string_lossy()); let mut to_delete = Vec::new(); - fn find_files_to_delete(pattern: &str, to_delete: &mut Vec) { + fn find_files_to_delete(pattern: &str, to_delete: &mut Vec) { match glob::glob(pattern) { Ok(paths) => { for path in paths.into_iter().flatten() { - to_delete.push(path.to_string_lossy().to_string()); + to_delete.push(path); } } Err(e) => { @@ -423,20 +424,19 @@ impl UpdateManager { if update.BaseRelease.is_some() && !update.DeltasToTarget.is_empty() { info!("Beginning delta update process."); - if let Err(e) = self.download_and_apply_delta_updates(update, &partial_file, progress.clone()) { - error!("Error downloading delta updates: {}", e); + if self.download_and_apply_delta_updates(update, &partial_file, progress.clone()).is_err() { info!("Falling back to full update..."); - self.source.download_release_entry(&update.TargetFullRelease, &partial_file.to_string_lossy(), progress)?; + self.source.download_release_entry(&update.TargetFullRelease, &partial_file, progress)?; self.verify_package_checksum(&partial_file, &update.TargetFullRelease)?; - info!("Successfully downloaded file: '{}'", partial_file.to_string_lossy()); + info!("Successfully downloaded file: '{:?}'", partial_file); } } else { - self.source.download_release_entry(&update.TargetFullRelease, &partial_file.to_string_lossy(), progress)?; + self.source.download_release_entry(&update.TargetFullRelease, &partial_file, progress)?; self.verify_package_checksum(&partial_file, &update.TargetFullRelease)?; - info!("Successfully downloaded file: '{}'", partial_file.to_string_lossy()); + info!("Successfully downloaded file: '{:?}'", partial_file); } - info!("Renaming partial file to final target: '{}'", final_target_file.to_string_lossy()); + info!("Renaming partial file to final target: '{:?}'", final_target_file); fs::rename(&partial_file, &final_target_file)?; find_files_to_delete(&delta_pattern, &mut to_delete); @@ -457,7 +457,7 @@ impl UpdateManager { } for path in to_delete { - info!("Deleting up old package: '{}'", path); + info!("Deleting up old package: '{:?}'", path); let _ = fs::remove_file(&path); } @@ -467,23 +467,21 @@ impl UpdateManager { fn download_and_apply_delta_updates( &self, update: &UpdateInfo, - target_file: &PathBuf, + output_file: &PathBuf, progress: Option>, ) -> Result<(), Error> { let packages_dir = self.locator.get_packages_dir(); let base_release_path = packages_dir.join(&update.BaseRelease.as_ref().unwrap().FileName); - let base_release_path = base_release_path.to_string_lossy().to_string(); - let output_path = target_file.to_string_lossy().to_string(); - let mut args: Vec = - ["patch", "--old", &base_release_path, "--output", &output_path].iter().map(|s| s.to_string()).collect(); + let mut args: Vec = + vec!["patch".into(), "--old".into(), base_release_path.clone().into(), "--output".into(), output_file.clone().into()]; for (i, delta) in update.DeltasToTarget.iter().enumerate() { let delta_file = packages_dir.join(&delta.FileName); let partial_file = delta_file.with_extension("partial"); info!("Downloading delta package: '{}'", &delta.FileName); - self.source.download_release_entry(&delta, &partial_file.to_string_lossy(), None)?; + self.source.download_release_entry(&delta, &partial_file, None)?; self.verify_package_checksum(&partial_file, delta)?; fs::rename(&partial_file, &delta_file)?; @@ -492,11 +490,11 @@ impl UpdateManager { let _ = progress.send(((i as f64 / update.DeltasToTarget.len() as f64) * 70.0) as i16); } - args.push("--delta".to_string()); - args.push(delta_file.to_string_lossy().to_string()); + args.push("--delta".into()); + args.push(delta_file.into()); } - info!("Applying {} patches to {}.", update.DeltasToTarget.len(), output_path); + info!("Applying {} patches to {:?}.", update.DeltasToTarget.len(), output_file); if let Some(progress) = &progress { let _ = progress.send(70); @@ -508,7 +506,7 @@ impl UpdateManager { } else { let error_message = String::from_utf8_lossy(&output.stderr); error!("Error applying delta updates: {}", error_message); - return Err(Error::Generic(error_message.to_string())); + return Err(Error::Io(std::io::Error::new(std::io::ErrorKind::Other, "Process exited with non-zero status"))); } if let Some(progress) = &progress { @@ -517,17 +515,17 @@ impl UpdateManager { Ok(()) } - fn verify_package_checksum(&self, file: &PathBuf, asset: &VelopackAsset) -> Result<(), Error> { + fn verify_package_checksum(&self, file: &Path, asset: &VelopackAsset) -> Result<(), Error> { let file_size = file.metadata()?.len(); if file_size != asset.Size { - error!("File size mismatch for file '{}': expected {}, got {}", file.to_string_lossy(), asset.Size, file_size); - return Err(Error::SizeInvalid(file.to_string_lossy().to_string(), asset.Size, file_size)); + error!("File size mismatch for file '{:?}': expected {}, got {}", file, asset.Size, file_size); + return Err(Error::SizeInvalid(file.to_path_buf(), asset.Size, file_size)); } let (sha1, _) = misc::calculate_sha1_sha256(file)?; if !sha1.eq_ignore_ascii_case(&asset.SHA1) { - error!("SHA1 checksum mismatch for file '{}': expected '{}', got '{}'", file.to_string_lossy(), asset.SHA1, sha1); - return Err(Error::ChecksumInvalid(file.to_string_lossy().to_string(), asset.SHA1.clone(), sha1)); + error!("SHA1 checksum mismatch for file '{:?}': expected '{}', got '{}'", file, asset.SHA1, sha1); + return Err(Error::ChecksumInvalid(file.to_path_buf(), asset.SHA1.clone(), sha1)); } Ok(()) } @@ -577,7 +575,7 @@ impl UpdateManager { pub fn apply_updates_and_restart_with_args(&self, to_apply: A, restart_args: C) -> Result<(), Error> where A: AsRef, - S: AsRef, + S: AsRef, C: IntoIterator, { self.wait_exit_then_apply_updates(to_apply, false, true, restart_args)?; @@ -602,7 +600,7 @@ impl UpdateManager { pub fn wait_exit_then_apply_updates(&self, to_apply: A, silent: bool, restart: bool, restart_args: C) -> Result<(), Error> where A: AsRef, - S: AsRef, + S: AsRef, C: IntoIterator, { self.unsafe_apply_updates(to_apply, silent, ApplyWaitMode::WaitCurrentProcess, restart, restart_args)?; @@ -622,50 +620,49 @@ impl UpdateManager { ) -> Result<(), Error> where A: AsRef, - S: AsRef, + S: AsRef, C: IntoIterator, { let to_apply = to_apply.as_ref(); let pkg_path = self.locator.get_packages_dir().join(&to_apply.FileName); - let pkg_path_str = pkg_path.to_string_lossy(); - - let mut args = Vec::new(); - args.push("apply".to_string()); - - args.push("--package".to_string()); - args.push(pkg_path_str.to_string()); if !pkg_path.exists() { - error!("Package does not exist on disk: '{}'", &pkg_path_str); - return Err(Error::FileNotFound(pkg_path_str.to_string())); + error!("Package does not exist on disk: '{:?}'", &pkg_path); + return Err(Error::FileNotFound(pkg_path)); } + let mut args: Vec = Vec::new(); + args.push("apply".into()); + + args.push("--package".into()); + args.push(pkg_path.into()); + match wait_mode { ApplyWaitMode::NoWait => {} ApplyWaitMode::WaitCurrentProcess => { - args.push("--waitPid".to_string()); - args.push(format!("{}", std::process::id())); + args.push("--waitPid".into()); + args.push(format!("{}", std::process::id()).into()); } ApplyWaitMode::WaitPid(pid) => { - args.push("--waitPid".to_string()); - args.push(format!("{}", pid)); + args.push("--waitPid".into()); + args.push(format!("{}", pid).into()); } } if silent { - args.push("--silent".to_string()); + args.push("--silent".into()); } if !restart { - args.push("--norestart".to_string()); + args.push("--norestart".into()); } - args.push("--root".to_string()); - args.push(self.locator.get_root_dir_as_string()); + args.push("--root".into()); + args.push(self.locator.get_root_dir().into()); - let restart_args: Vec = restart_args.into_iter().map(|item| item.as_ref().to_string()).collect(); + let restart_args: Vec = restart_args.into_iter().map(|item| item.as_ref().to_os_string()).collect(); if !restart_args.is_empty() { - args.push("--".to_string()); + args.push("--".into()); for arg in restart_args { args.push(arg); } diff --git a/src/lib-rust/src/process_win.rs b/src/lib-rust/src/process_win.rs index f5fc0637..b7abdd1a 100644 --- a/src/lib-rust/src/process_win.rs +++ b/src/lib-rust/src/process_win.rs @@ -1,8 +1,9 @@ +use crate::wide_strings::*; use std::{ collections::HashMap, ffi::{OsStr, OsString}, io::{Error as IoError, ErrorKind as IoErrorKind, Result as IoResult}, - os::{raw::c_void, windows::ffi::OsStrExt}, + os::windows::ffi::OsStrExt, path::Path, time::Duration, }; @@ -101,7 +102,7 @@ fn append_arg(cmd: &mut Vec, arg: &Arg, force_quotes: bool) -> IoResult<()> Ok(()) } -fn make_command_line(argv0: Option<&OsStr>, args: &[Arg], force_quotes: bool) -> IoResult> { +fn make_command_line(argv0: Option<&OsStr>, args: &[Arg], force_quotes: bool) -> IoResult { // Encode the command and arguments in a command line string such // that the spawned process may recover them using CommandLineToArgvW. let mut cmd: Vec = Vec::new(); @@ -123,10 +124,10 @@ fn make_command_line(argv0: Option<&OsStr>, args: &[Arg], force_quotes: bool) -> } cmd.push(0); - Ok(cmd) + Ok(cmd.into()) } -fn make_envp(maybe_env: Option>) -> IoResult<(Option<*const c_void>, Vec)> { +fn make_envp(maybe_env: Option>) -> IoResult> { // On Windows we pass an "environment block" which is not a char**, but // rather a concatenation of null-terminated k=v\0 sequences, with a final // \0 to terminate. @@ -148,9 +149,9 @@ fn make_envp(maybe_env: Option>) -> IoResult<(Option<*co blk.push(0); } blk.push(0); - Ok((Some(blk.as_ptr() as *mut c_void), blk)) + Ok(Some(blk.into())) } else { - Ok((None, Vec::new())) + Ok(None) } } @@ -232,36 +233,18 @@ impl AsRef for SafeProcessHandle { // } // } -fn os_to_pcwstr>(d: P) -> IoResult<(PCWSTR, Vec)> { - let d = d.as_ref(); - let d = OsString::from(d); - let mut d_str: Vec = ensure_no_nuls(d)?.encode_wide().collect(); - d_str.push(0); - Ok((PCWSTR(d_str.as_ptr()), d_str)) -} - -fn pathopt_to_pcwstr>(d: Option

) -> IoResult<(PCWSTR, Vec)> { - match d { - Some(dir) => { - let dir = dir.as_ref(); - os_to_pcwstr(dir) - } - None => Ok((PCWSTR::null(), Vec::new())), - } -} - pub fn run_process_as_admin, P2: AsRef>( exe_path: P1, - args: Vec, + args: Vec, work_dir: Option, show_window: bool, ) -> IoResult { - let verb = os_to_pcwstr("runas")?; - let exe = os_to_pcwstr(exe_path.as_ref())?; + let verb = string_to_wide("runas"); + let exe = string_to_wide(exe_path.as_ref()); let wrapped_args: Vec = args.iter().map(|a| Arg::Regular(a.into())).collect(); let params = make_command_line(None, &wrapped_args, false)?; let params = PCWSTR(params.as_ptr()); - let work_dir = pathopt_to_pcwstr(work_dir.as_ref())?; + let work_dir = string_to_wide_opt(work_dir.map(|w| w.as_ref().to_path_buf())); let n_show = if show_window { windows::Win32::UI::WindowsAndMessaging::SW_NORMAL.0 @@ -272,10 +255,10 @@ pub fn run_process_as_admin, P2: AsRef>( let mut exe_info: SHELLEXECUTEINFOW = SHELLEXECUTEINFOW { cbSize: std::mem::size_of::() as u32, fMask: SEE_MASK_NOCLOSEPROCESS, - lpVerb: verb.0, - lpFile: exe.0, + lpVerb: verb.as_pcwstr(), + lpFile: exe.as_pcwstr(), lpParameters: params, - lpDirectory: work_dir.0, + lpDirectory: work_dir.as_ref().map(|d| d.as_pcwstr()).unwrap_or(PCWSTR::default()), nShow: n_show, ..Default::default() }; @@ -291,20 +274,16 @@ pub fn run_process_as_admin, P2: AsRef>( pub fn run_process, P2: AsRef>( exe_path: P1, - args: Vec, + args: Vec, work_dir: Option, show_window: bool, set_env: Option>, ) -> IoResult { - let exe_path = exe_path.as_ref(); - let exe_path = OsString::from(exe_path); - let exe_name_ptr = os_to_pcwstr(&exe_path)?; - - let work_dir = work_dir.map(|d| d.as_ref().to_path_buf()); - + let exe_path = string_to_wide(exe_path.as_ref()); + let dirp = string_to_wide_opt(work_dir.map(|w| w.as_ref().to_path_buf())); + let envp = make_envp(set_env)?; let wrapped_args: Vec = args.iter().map(|a| Arg::Regular(a.into())).collect(); - let mut params = make_command_line(Some(&exe_path), &wrapped_args, false)?; - let params = PWSTR(params.as_mut_ptr()); + let mut params = make_command_line(Some(exe_path.as_os_str()), &wrapped_args, false)?; let mut pi = windows::Win32::System::Threading::PROCESS_INFORMATION::default(); @@ -329,9 +308,6 @@ pub fn run_process, P2: AsRef>( hStdError: HANDLE(std::ptr::null_mut()), }; - let envp = make_envp(set_env)?; - let dirp = pathopt_to_pcwstr(work_dir.as_deref())?; - let flags = if show_window { CREATE_UNICODE_ENVIRONMENT } else { @@ -339,8 +315,19 @@ pub fn run_process, P2: AsRef>( }; unsafe { - info!("About to launch: '{:?}' in dir '{:?}' with arguments: {:?}", exe_path, work_dir, args); - CreateProcessW(exe_name_ptr.0, Option::Some(params), None, None, false, flags, envp.0, dirp.0, &si, &mut pi)?; + info!("About to launch: '{:?}' in dir '{:?}' with arguments: {:?}", exe_path, dirp, args); + CreateProcessW( + exe_path.as_pcwstr(), + Some(params.as_pwstr()), + None, + None, + false, + flags, + envp.map(|e| e.as_cvoid()), + dirp.map(|d| d.as_pcwstr()).unwrap_or(PCWSTR::default()), + &si, + &mut pi, + )?; let _ = AllowSetForegroundWindow(pi.dwProcessId); let _ = CloseHandle(pi.hThread); } diff --git a/src/lib-rust/src/sources.rs b/src/lib-rust/src/sources.rs index 635bc2bb..566f5731 100644 --- a/src/lib-rust/src/sources.rs +++ b/src/lib-rust/src/sources.rs @@ -3,8 +3,8 @@ use std::{ sync::mpsc::Sender, }; -use crate::*; use crate::bundle::Manifest; +use crate::*; /// Abstraction for finding and downloading updates from a package source / repository. /// An implementation may copy a file from a local repository, download from a web address, @@ -14,7 +14,7 @@ pub trait UpdateSource: Send + Sync { /// can subsequently be downloaded with download_release_entry. fn get_release_feed(&self, channel: &str, app: &bundle::Manifest, staged_user_id: &str) -> Result; /// Download the specified VelopackAsset to the provided local file path. - fn download_release_entry(&self, asset: &VelopackAsset, local_file: &str, progress_sender: Option>) -> Result<(), Error>; + fn download_release_entry(&self, asset: &VelopackAsset, local_file: &Path, progress_sender: Option>) -> Result<(), Error>; /// Clone the source to create a new lifetime. fn clone_boxed(&self) -> Box; } @@ -25,16 +25,21 @@ impl Clone for Box { } } -/// A source that does not provide any update capability. +/// A source that does not provide any update capability. #[derive(Clone)] pub struct NoneSource {} impl UpdateSource for NoneSource { fn get_release_feed(&self, _channel: &str, _app: &Manifest, _staged_user_id: &str) -> Result { - Err(Error::Generic("None source does not checking release feed".to_owned())) + Err(Error::NotSupported("None source does not checking release feed".to_owned())) } - fn download_release_entry(&self, _asset: &VelopackAsset, _local_file: &str, _progress_sender: Option>) -> Result<(), Error> { - Err(Error::Generic("None source does not support downloads".to_owned())) + fn download_release_entry( + &self, + _asset: &VelopackAsset, + _local_file: &Path, + _progress_sender: Option>, + ) -> Result<(), Error> { + Err(Error::NotSupported("None source does not support downloads".to_owned())) } fn clone_boxed(&self) -> Box { Box::new(self.clone()) @@ -72,7 +77,7 @@ impl UpdateSource for AutoSource { self.source.get_release_feed(channel, app, staged_user_id) } - fn download_release_entry(&self, asset: &VelopackAsset, local_file: &str, progress_sender: Option>) -> Result<(), Error> { + fn download_release_entry(&self, asset: &VelopackAsset, local_file: &Path, progress_sender: Option>) -> Result<(), Error> { self.source.download_release_entry(asset, local_file, progress_sender) } @@ -111,12 +116,12 @@ impl UpdateSource for HttpSource { Ok(feed) } - fn download_release_entry(&self, asset: &VelopackAsset, local_file: &str, progress_sender: Option>) -> Result<(), Error> { + fn download_release_entry(&self, asset: &VelopackAsset, local_file: &Path, progress_sender: Option>) -> Result<(), Error> { let path = self.url.trim_end_matches('/').to_owned() + "/"; let url = url::Url::parse(&path)?; let asset_url = url.join(&asset.FileName)?; - info!("About to download from URL '{}' to file '{}'", asset_url, local_file); + info!("About to download from URL '{}' to file '{:?}'", asset_url, local_file); download::download_url_to_file(asset_url.as_str(), local_file, move |p| { if let Some(progress_sender) = &progress_sender { let _ = progress_sender.send(p); @@ -150,15 +155,15 @@ impl UpdateSource for FileSource { let releases_name = format!("releases.{}.json", channel); let releases_path = self.path.join(&releases_name); - info!("Reading releases from file: {}", releases_path.display()); + info!("Reading releases from file: {:?}", releases_path); let json = std::fs::read_to_string(releases_path)?; let feed: VelopackAssetFeed = serde_json::from_str(&json)?; Ok(feed) } - fn download_release_entry(&self, asset: &VelopackAsset, local_file: &str, progress_sender: Option>) -> Result<(), Error> { + fn download_release_entry(&self, asset: &VelopackAsset, local_file: &Path, progress_sender: Option>) -> Result<(), Error> { let asset_path = self.path.join(&asset.FileName); - info!("About to copy from file '{}' to file '{}'", asset_path.display(), local_file); + info!("About to copy from file '{:?}' to file '{:?}'", asset_path, local_file); if let Some(progress_sender) = &progress_sender { let _ = progress_sender.send(50); } diff --git a/src/lib-rust/src/wide_strings.rs b/src/lib-rust/src/wide_strings.rs new file mode 100644 index 00000000..262d85a9 --- /dev/null +++ b/src/lib-rust/src/wide_strings.rs @@ -0,0 +1,190 @@ +use std::{ + ffi::{c_void, OsStr, OsString}, + fmt::Debug, + os::windows::ffi::{OsStrExt, OsStringExt}, +}; +use windows::core::{PCWSTR, PWSTR}; + +#[derive(Clone, PartialEq, Eq)] +pub struct WideString { + str: OsString, + vec: Vec, +} + +impl WideString { + pub fn as_slice(&self) -> &[u16] { + &self.vec + } + + pub fn as_mut_slice(&mut self) -> &mut [u16] { + &mut self.vec + } + + pub fn len(&self) -> usize { + self.vec.len() + } + + pub fn as_os_str(&self) -> &OsStr { + self.str.as_os_str() + } + + pub fn as_cvoid(&self) -> *const c_void { + self.as_ptr() as *const c_void + } + + pub fn as_mut_cvoid(&mut self) -> *const c_void { + self.as_mut_ptr() as *const c_void + } + + pub fn as_ptr(&self) -> *const u16 { + self.as_slice().as_ptr() + } + + pub fn as_mut_ptr(&mut self) -> *mut u16 { + self.as_mut_slice().as_mut_ptr() + } + + pub fn as_pcwstr(&self) -> PCWSTR { + PCWSTR(self.as_ptr()) + } + + pub fn as_pwstr(&mut self) -> PWSTR { + PWSTR(self.as_mut_ptr()) + } +} + +impl Debug for WideString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "WideString({:?})", self.str) + } +} + +impl From> for WideString { + fn from(inner: Vec) -> Self { + WideString { str: OsString::from_wide(&inner), vec: inner } + } +} + +impl From for WideString { + fn from(inner: String) -> Self { + string_to_wide(inner) + } +} + +impl From<&str> for WideString { + fn from(inner: &str) -> Self { + string_to_wide(inner) + } +} + +impl Into> for WideString { + fn into(self) -> Vec { + self.vec + } +} + +impl AsRef for WideString { + fn as_ref(&self) -> &OsStr { + self.str.as_os_str() + } +} + +impl AsRef<[u16]> for WideString { + fn as_ref(&self) -> &[u16] { + &self.vec + } +} + +impl AsMut<[u16]> for WideString { + fn as_mut(&mut self) -> &mut [u16] { + &mut self.vec + } +} + +pub fn string_to_wide>(input: P) -> WideString { + let str = input.as_ref(); + let str = OsString::from(str); + let vec = str + .encode_wide() + .filter(|f| *f != 0) // Filter out any null characters + .chain(Some(0)) + .collect::>(); + WideString { vec, str } +} + +pub fn string_to_wide_opt>(input: Option

) -> Option { + input.map(string_to_wide) +} + +pub trait ToWideSlice { + fn to_wide_slice(&self) -> &[u16]; +} + +impl ToWideSlice for PWSTR { + fn to_wide_slice(&self) -> &[u16] { + unsafe { self.as_wide() } + } +} + +impl ToWideSlice for PCWSTR { + fn to_wide_slice(&self) -> &[u16] { + unsafe { self.as_wide() } + } +} + +impl ToWideSlice for Vec { + fn to_wide_slice(&self) -> &[u16] { + self.as_ref() + } +} + +impl ToWideSlice for &Vec { + fn to_wide_slice(&self) -> &[u16] { + self.as_ref() + } +} + +impl ToWideSlice for &[u16] { + fn to_wide_slice(&self) -> &[u16] { + self + } +} + +impl ToWideSlice for [u16; N] { + fn to_wide_slice(&self) -> &[u16] { + self.as_ref() + } +} + +pub fn wide_to_string_lossy(input: T) -> String { + let slice = input.to_wide_slice(); + let null_pos = slice.iter().position(|&x| x == 0).unwrap_or(slice.len()); + let trimmed_slice = &slice[..null_pos]; + String::from_utf16_lossy(trimmed_slice) +} + +pub fn wide_to_string_lossy_opt(input: Option) -> Option { + input.map(wide_to_string_lossy) +} + +pub fn wide_to_string(input: T) -> Result { + let slice = input.to_wide_slice(); + let null_pos = slice.iter().position(|&x| x == 0).unwrap_or(slice.len()); + let trimmed_slice = &slice[..null_pos]; + Ok(String::from_utf16(trimmed_slice)?) +} + +pub fn wide_to_string_opt(input: Option) -> Option> { + input.map(wide_to_string) +} + +pub fn wide_to_os_string(input: T) -> OsString { + let slice = input.to_wide_slice(); + let null_pos = slice.iter().position(|&x| x == 0).unwrap_or(slice.len()); + let trimmed_slice = &slice[..null_pos]; + OsString::from_wide(trimmed_slice) +} + +pub fn wide_to_os_string_opt(input: Option) -> Option { + input.map(wide_to_os_string) +}