Merge pull request #5440 from dotCooCoo/fix/photocraft-app-paths-unicode

Edit Original: read PhotoCraft's App Paths entry as UTF-16 on Windows
This commit is contained in:
BFlat
2026-11-04 09:17:24 +07:00
committed by GitHub
2 changed files with 64 additions and 30 deletions
+3 -2
View File
@@ -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 }
+61 -28
View File
@@ -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(&reg).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(&reg).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");
}
}