Units: read the Windows measurement system from the registry, not reg.exe (#1227)

The desktop app started `reg.exe query "HKCU\Control Panel\International" /v iMeasure` at every launch to pick the default Units (#463). On managed PCs reg.exe is a process EDR and WDAC/AppLocker setups watch, and a Sandbox process-creation audit showed it as the one child process left at startup. Read the value in-process with winsafe's safe RegGetValue (advapi feature) instead. Behaviour is unchanged: "1" is inches, "0" is millimetres, anything missing or unreadable falls through to the locale check. The value-to-unit mapping is a pure function with a unit test, plus a Windows test that the registry read yields a known value.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jcl-bit 2026-10-11 22:33:41 -07:00 committed by GitHub
parent 4812e49992
commit 26a68d9765
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 52 additions and 19 deletions

1
Cargo.lock generated
View File

@ -3982,6 +3982,7 @@ dependencies = [
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"winsafe",
]
[[package]]

View File

@ -51,6 +51,8 @@ objc2-foundation = { version = "0.3", default-features = false, features = ["std
[target.'cfg(windows)'.dependencies]
# Auto interface language: the Windows display language, without starting a process.
sys-locale = "0.3"
# Region ▸ Measurement system for the default Units, read from the registry without starting reg.exe.
winsafe = { version = "0.0.29", default-features = false, features = ["advapi"] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
rfd = { workspace = true, features = ["file-handle-inner"] }

View File

@ -99,29 +99,37 @@ fn detect() -> Option<Unit> {
{
return Some(unit);
}
// Region ▸ Measurement system, read in-process: a child `reg.exe` is a process-creation event
// that endpoint protection watches on managed PCs, and a preference doesn't warrant one.
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
// Region ▸ Measurement system: 0 metric, 1 U.S. The absolute path keeps a `reg` earlier on
// PATH from running; CREATE_NO_WINDOW keeps a console from flashing up.
let windir = std::env::var_os("SystemRoot").unwrap_or_else(|| "C:\\Windows".into());
let reg = std::path::Path::new(&windir).join("System32").join("reg.exe");
if let Ok(out) = std::process::Command::new(reg)
.args(["query", "HKCU\\Control Panel\\International", "/v", "iMeasure"])
.creation_flags(0x0800_0000)
.output()
&& out.status.success()
{
match String::from_utf8_lossy(&out.stdout).split_whitespace().last() {
Some("1") => return Some(Unit::Inches),
Some("0") => return Some(Unit::Millimetres),
_ => {}
}
}
if let Some(unit) = windows_measure().and_then(|v| unit_for_imeasure(&v)) {
return Some(unit);
}
["LC_ALL", "LC_MEASUREMENT", "LANG"].iter().find_map(|var| std::env::var(var).ok().filter(|v| !v.is_empty()).and_then(|v| unit_for_tag(&v)))
}
/// The unit for the Windows `iMeasure` value (Region ▸ Measurement system): "0" metric, "1" U.S.
/// `None` for anything else, so a missing or odd value falls through to the locale check.
#[cfg(any(test, target_os = "windows"))]
pub(crate) fn unit_for_imeasure(value: &str) -> Option<Unit> {
match value.trim() {
"1" => Some(Unit::Inches),
"0" => Some(Unit::Millimetres),
_ => None,
}
}
/// `HKCU\Control Panel\International\iMeasure` as text; `None` when it is missing, unreadable or
/// not a string.
#[cfg(target_os = "windows")]
fn windows_measure() -> Option<String> {
use winsafe::{HKEY, RegistryValue, co::RRF};
match HKEY::CURRENT_USER.RegGetValue(Some("Control Panel\\International"), Some("iMeasure"), RRF::RT_REG_SZ).ok()? {
RegistryValue::Sz(v) => Some(v),
_ => None,
}
}
#[cfg(target_arch = "wasm32")]
fn detect() -> Option<Unit> {
None
@ -129,7 +137,7 @@ fn detect() -> Option<Unit> {
#[cfg(test)]
mod tests {
use super::{Unit, unit_for_tag};
use super::{Unit, unit_for_imeasure, unit_for_tag};
#[test]
fn region_decides_the_unit() {
@ -146,6 +154,28 @@ mod tests {
assert_eq!(unit_for_tag(""), None);
}
#[test]
fn windows_measurement_system_decides_the_unit() {
assert_eq!(unit_for_imeasure("1"), Some(Unit::Inches));
assert_eq!(unit_for_imeasure("0"), Some(Unit::Millimetres));
assert_eq!(unit_for_imeasure(" 1\r\n"), Some(Unit::Inches));
assert_eq!(unit_for_imeasure(""), None);
assert_eq!(unit_for_imeasure("2"), None);
assert_eq!(unit_for_imeasure("metric"), None);
}
/// The registry read works in-process (no `reg.exe`) and gives a value the mapping understands.
#[cfg(target_os = "windows")]
#[test]
fn windows_reads_imeasure_in_process() {
let value = super::windows_measure();
// Every Windows user profile has Region settings; if a sandbox ever lacks them, the read must
// simply be None rather than failing.
if let Some(v) = value {
assert!(unit_for_imeasure(&v).is_some(), "unexpected iMeasure {v:?}");
}
}
#[test]
fn a4_in_every_unit() {
assert_eq!(Unit::Millimetres.size(595.276, 841.89), "210.0 × 297.0 mm");