mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 02:19:56 -06:00
Edit Original: read PhotoCraft's App Paths entry as UTF-16 on Windows
The lookup ran reg.exe and decoded its output as UTF-8, but reg.exe writes in the console's code page, so a registered path with characters outside ASCII came back wrong, and characters outside that code page came back as "?". The path is now read with RegGetValueW.
This commit is contained in:
@@ -56,8 +56,9 @@ pollster = "1"
|
||||
# `GetPointerPenInfo`, which it uses to read the pen's pressure and which end of the pen is down.
|
||||
# Win32_System_Threading has `GetProcessAffinityMask`, which src/windows_cores.rs uses to count the
|
||||
# cores the process may run on. Win32_UI_Input_KeyboardAndMouse has `GetKeyState`, which
|
||||
# src/caps_lock.rs uses to read whether Caps Lock is on.
|
||||
windows = { version = "0.62", features = ["Win32_Graphics_DirectWrite", "Win32_Graphics_Gdi", "Win32_System_Threading", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Input_Pointer", "Win32_UI_WindowsAndMessaging"] }
|
||||
# src/caps_lock.rs uses to read whether Caps Lock is on. Win32_System_Registry has `RegGetValueW`,
|
||||
# which src/host_services.rs uses to read the path PhotoCraft registers under App Paths.
|
||||
windows = { version = "0.62", features = ["Win32_Graphics_DirectWrite", "Win32_Graphics_Gdi", "Win32_System_Registry", "Win32_System_Threading", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Input_Pointer", "Win32_UI_WindowsAndMessaging"] }
|
||||
# The renderer's worker count follows those cores (src/windows_cores.rs).
|
||||
vectorcraft-render = { workspace = true }
|
||||
|
||||
|
||||
@@ -461,24 +461,13 @@ pub(crate) fn windows_candidates(
|
||||
}
|
||||
|
||||
/// The paths PhotoCraft's installer registers under App Paths, for the current user and then for
|
||||
/// everyone (`reg query`, so no extra dependency; a hidden console window).
|
||||
/// everyone.
|
||||
#[cfg(windows)]
|
||||
fn registered_photocraft() -> Vec<std::path::PathBuf> {
|
||||
use std::os::windows::process::CommandExt as _;
|
||||
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
||||
// reg.exe by its full path, so nothing named `reg` in the app's folder or on PATH runs instead.
|
||||
let root = std::env::var_os("SystemRoot").map(std::path::PathBuf::from).unwrap_or_else(|| r"C:\Windows".into());
|
||||
["HKCU", "HKLM"]
|
||||
use windows::Win32::System::Registry::{HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE};
|
||||
[HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE]
|
||||
.into_iter()
|
||||
.filter_map(|hive| {
|
||||
let key = format!(r"{hive}\Software\Microsoft\Windows\CurrentVersion\App Paths\photocraft.exe");
|
||||
let out = std::process::Command::new(root.join("System32").join("reg.exe"))
|
||||
.args(["query", &key, "/ve"])
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.output()
|
||||
.ok()?;
|
||||
registered_path(&String::from_utf8_lossy(&out.stdout))
|
||||
})
|
||||
.filter_map(|hive| registry_default_path(hive, r"Software\Microsoft\Windows\CurrentVersion\App Paths\photocraft.exe"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -487,11 +476,35 @@ fn registered_photocraft() -> Vec<std::path::PathBuf> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
/// The path in `reg query … /ve` output like
|
||||
/// ` (Default) REG_SZ C:\Program Files\ArtCraft\PhotoCraft\photocraft.exe`.
|
||||
/// The default `REG_SZ` value of `subkey` under `hive` as a path ([`registered_path`]), or `None`
|
||||
/// when the key or the value is missing. The value is read as UTF-16, so a folder name in any
|
||||
/// script comes back as stored.
|
||||
#[cfg(windows)]
|
||||
#[allow(unsafe_code)]
|
||||
fn registry_default_path(hive: windows::Win32::System::Registry::HKEY, subkey: &str) -> Option<std::path::PathBuf> {
|
||||
use windows::Win32::System::Registry::{RRF_RT_REG_SZ, RegGetValueW};
|
||||
use windows::core::PCWSTR;
|
||||
let subkey: Vec<u16> = subkey.encode_utf16().chain([0]).collect();
|
||||
let mut size = 0u32;
|
||||
// SAFETY: `subkey` is a NUL-terminated UTF-16 string that outlives the call, a null value name
|
||||
// reads the default value, and with no buffer the call only writes the value's size to `size`.
|
||||
unsafe { RegGetValueW(hive, PCWSTR(subkey.as_ptr()), PCWSTR::null(), RRF_RT_REG_SZ, None, None, Some(&mut size)) }.ok().ok()?;
|
||||
let mut value = vec![0u16; (size as usize).div_ceil(2)];
|
||||
let mut size = u32::try_from(value.len() * 2).ok()?;
|
||||
// SAFETY: as above, and `value` holds the `size` bytes the call may write.
|
||||
unsafe { RegGetValueW(hive, PCWSTR(subkey.as_ptr()), PCWSTR::null(), RRF_RT_REG_SZ, None, Some(value.as_mut_ptr().cast()), Some(&mut size)) }
|
||||
.ok()
|
||||
.ok()?;
|
||||
registered_path(&value)
|
||||
}
|
||||
|
||||
/// The path in a registry string value read as UTF-16, up to its first NUL, without surrounding
|
||||
/// spaces or quotes, like `C:\Program Files\ArtCraft\PhotoCraft\photocraft.exe`.
|
||||
#[cfg_attr(not(any(windows, test)), allow(dead_code))]
|
||||
pub(crate) fn registered_path(text: &str) -> Option<std::path::PathBuf> {
|
||||
text.lines().find_map(|l| l.split_once("REG_SZ")).map(|(_, v)| v.trim().trim_matches('"')).filter(|v| !v.is_empty()).map(std::path::PathBuf::from)
|
||||
pub(crate) fn registered_path(value: &[u16]) -> Option<std::path::PathBuf> {
|
||||
let end = value.iter().position(|&c| c == 0).unwrap_or(value.len());
|
||||
let text = String::from_utf16(&value[..end]).ok()?;
|
||||
Some(text.trim().trim_matches('"')).filter(|v| !v.is_empty()).map(std::path::PathBuf::from)
|
||||
}
|
||||
|
||||
/// Is `app` an application Edit Original may start: an `.app` bundle on macOS, an `.exe` on
|
||||
@@ -616,17 +629,37 @@ mod open_with_tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// The registered path is read from `reg query … /ve` output, spaces and quotes included;
|
||||
/// no value, no path.
|
||||
/// The registered path is read from the UTF-16 value up to its NUL, spaces, quotes and
|
||||
/// characters outside ASCII included; an empty value or one that is not UTF-16 gives no path.
|
||||
#[test]
|
||||
fn the_registered_path_is_read_from_reg_query() {
|
||||
let out = "\r\nHKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\photocraft.exe\r\n (Default) REG_SZ C:\\Program Files\\ArtCraft\\PhotoCraft\\photocraft.exe\r\n";
|
||||
assert_eq!(registered_path(out), Some(std::path::PathBuf::from(r"C:\Program Files\ArtCraft\PhotoCraft\photocraft.exe")));
|
||||
fn the_registered_path_is_read_from_the_utf16_value() {
|
||||
let wide = |s: &str| s.encode_utf16().chain([0, 0]).collect::<Vec<u16>>();
|
||||
assert_eq!(
|
||||
registered_path(" (Standard) REG_SZ \"C:\\A B\\photocraft.exe\""),
|
||||
Some(std::path::PathBuf::from(r"C:\A B\photocraft.exe"))
|
||||
registered_path(&wide(r"C:\Program Files\ArtCraft\PhotoCraft\photocraft.exe")),
|
||||
Some(std::path::PathBuf::from(r"C:\Program Files\ArtCraft\PhotoCraft\photocraft.exe"))
|
||||
);
|
||||
assert_eq!(registered_path("ERROR: The system was unable to find the specified registry key or value."), None);
|
||||
assert_eq!(registered_path(" (Default) REG_SZ "), None);
|
||||
assert_eq!(
|
||||
registered_path(&wide(" \"C:\\Prögram Fïles\\Тест\\写真\\photocraft.exe\" ")),
|
||||
Some(std::path::PathBuf::from(r"C:\Prögram Fïles\Тест\写真\photocraft.exe"))
|
||||
);
|
||||
assert_eq!(registered_path(&wide("")), None);
|
||||
assert_eq!(registered_path(&[0xD800, 0]), None);
|
||||
}
|
||||
|
||||
/// On Windows a registered path with characters outside the console's code page is read as
|
||||
/// stored, from a scratch key under the current user's hive.
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn a_registered_path_outside_ascii_is_read_as_stored() {
|
||||
let key = format!(r"Software\VectorCraftTest-app-paths-{}", std::process::id());
|
||||
let path = r"C:\Prögram Fïles\Тест\写真\photocraft.exe";
|
||||
let reg = std::env::var_os("SystemRoot").map(std::path::PathBuf::from).unwrap_or_else(|| r"C:\Windows".into()).join(r"System32\reg.exe");
|
||||
let full = format!(r"HKCU\{key}");
|
||||
let added = std::process::Command::new(®).args(["add", &full, "/ve", "/t", "REG_SZ", "/d", path, "/f"]).output().unwrap();
|
||||
assert!(added.status.success(), "{added:?}");
|
||||
let read = super::registry_default_path(windows::Win32::System::Registry::HKEY_CURRENT_USER, &key);
|
||||
let _ = std::process::Command::new(®).args(["delete", &full, "/f"]).output();
|
||||
assert_eq!(read, Some(std::path::PathBuf::from(path)));
|
||||
assert_eq!(super::registry_default_path(windows::Win32::System::Registry::HKEY_CURRENT_USER, &key), None, "deleted");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user