Skip to content

Commit 523bdb8

Browse files
committed
Windows Command environment variables are case-preserving
But comparing is case-insensitive.
1 parent fdaa765 commit 523bdb8

File tree

3 files changed

+128
-9
lines changed

3 files changed

+128
-9
lines changed

std/src/sys/windows/c.rs

+12
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ pub type ADDRESS_FAMILY = USHORT;
6868
pub const TRUE: BOOL = 1;
6969
pub const FALSE: BOOL = 0;
7070

71+
pub const CSTR_LESS_THAN: c_int = 1;
72+
pub const CSTR_EQUAL: c_int = 2;
73+
pub const CSTR_GREATER_THAN: c_int = 3;
74+
7175
pub const FILE_ATTRIBUTE_READONLY: DWORD = 0x1;
7276
pub const FILE_ATTRIBUTE_DIRECTORY: DWORD = 0x10;
7377
pub const FILE_ATTRIBUTE_REPARSE_POINT: DWORD = 0x400;
@@ -1072,6 +1076,14 @@ extern "system" {
10721076
pub fn ReleaseSRWLockShared(SRWLock: PSRWLOCK);
10731077
pub fn TryAcquireSRWLockExclusive(SRWLock: PSRWLOCK) -> BOOLEAN;
10741078
pub fn TryAcquireSRWLockShared(SRWLock: PSRWLOCK) -> BOOLEAN;
1079+
1080+
pub fn CompareStringOrdinal(
1081+
lpString1: LPCWSTR,
1082+
cchCount1: c_int,
1083+
lpString2: LPCWSTR,
1084+
cchCount2: c_int,
1085+
bIgnoreCase: BOOL,
1086+
) -> c_int;
10751087
}
10761088

10771089
// Functions that aren't available on every version of Windows that we support,

std/src/sys/windows/process.rs

+55-9
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
mod tests;
55

66
use crate::borrow::Borrow;
7+
use crate::cmp;
78
use crate::collections::BTreeMap;
89
use crate::convert::{TryFrom, TryInto};
910
use crate::env;
@@ -34,32 +35,76 @@ use libc::{c_void, EXIT_FAILURE, EXIT_SUCCESS};
3435
// Command
3536
////////////////////////////////////////////////////////////////////////////////
3637

37-
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
38+
#[derive(Clone, Debug, Eq)]
3839
#[doc(hidden)]
39-
pub struct EnvKey(OsString);
40+
pub struct EnvKey {
41+
os_string: OsString,
42+
// This stores a UTF-16 encoded string to workaround the mismatch between
43+
// Rust's OsString (WTF-8) and the Windows API string type (UTF-16).
44+
// Normally converting on every API call is acceptable but here
45+
// `c::CompareStringOrdinal` will be called for every use of `==`.
46+
utf16: Vec<u16>,
47+
}
48+
49+
// Windows environment variables preserve their case but comparisons use
50+
// simplified case folding. So we call `CompareStringOrdinal` to get the OS to
51+
// perform the comparison.
52+
impl Ord for EnvKey {
53+
fn cmp(&self, other: &Self) -> cmp::Ordering {
54+
unsafe {
55+
let result = c::CompareStringOrdinal(
56+
self.utf16.as_ptr(),
57+
self.utf16.len() as _,
58+
other.utf16.as_ptr(),
59+
other.utf16.len() as _,
60+
c::TRUE,
61+
);
62+
match result {
63+
c::CSTR_LESS_THAN => cmp::Ordering::Less,
64+
c::CSTR_EQUAL => cmp::Ordering::Equal,
65+
c::CSTR_GREATER_THAN => cmp::Ordering::Greater,
66+
// `CompareStringOrdinal` should never fail so long as the parameters are correct.
67+
_ => panic!("comparing environment keys failed: {}", Error::last_os_error()),
68+
}
69+
}
70+
}
71+
}
72+
impl PartialOrd for EnvKey {
73+
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
74+
Some(self.cmp(other))
75+
}
76+
}
77+
impl PartialEq for EnvKey {
78+
fn eq(&self, other: &Self) -> bool {
79+
if self.utf16.len() != other.utf16.len() {
80+
false
81+
} else {
82+
self.cmp(other) == cmp::Ordering::Equal
83+
}
84+
}
85+
}
4086

4187
impl From<OsString> for EnvKey {
42-
fn from(mut k: OsString) -> Self {
43-
k.make_ascii_uppercase();
44-
EnvKey(k)
88+
fn from(k: OsString) -> Self {
89+
EnvKey { utf16: k.encode_wide().collect(), os_string: k }
4590
}
4691
}
4792

4893
impl From<EnvKey> for OsString {
4994
fn from(k: EnvKey) -> Self {
50-
k.0
95+
k.os_string
5196
}
5297
}
5398

5499
impl Borrow<OsStr> for EnvKey {
55100
fn borrow(&self) -> &OsStr {
56-
&self.0
101+
&self.os_string
57102
}
58103
}
59104

60105
impl AsRef<OsStr> for EnvKey {
61106
fn as_ref(&self) -> &OsStr {
62-
&self.0
107+
&self.os_string
63108
}
64109
}
65110

@@ -531,7 +576,8 @@ fn make_envp(maybe_env: Option<BTreeMap<EnvKey, OsString>>) -> io::Result<(*mut
531576
let mut blk = Vec::new();
532577

533578
for (k, v) in env {
534-
blk.extend(ensure_no_nuls(k.0)?.encode_wide());
579+
ensure_no_nuls(k.os_string)?;
580+
blk.extend(k.utf16);
535581
blk.push('=' as u16);
536582
blk.extend(ensure_no_nuls(v)?.encode_wide());
537583
blk.push(0);

std/src/sys/windows/process/tests.rs

+61
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use super::make_command_line;
2+
use crate::env;
23
use crate::ffi::{OsStr, OsString};
4+
use crate::process::Command;
35

46
#[test]
57
fn test_make_command_line() {
@@ -41,3 +43,62 @@ fn test_make_command_line() {
4143
"\"\u{03c0}\u{042f}\u{97f3}\u{00e6}\u{221e}\""
4244
);
4345
}
46+
47+
// On Windows, environment args are case preserving but comparisons are case-insensitive.
48+
// See: #85242
49+
#[test]
50+
fn windows_env_unicode_case() {
51+
let test_cases = [
52+
("ä", "Ä"),
53+
("ß", "SS"),
54+
("Ä", "Ö"),
55+
("Ä", "Ö"),
56+
("I", "İ"),
57+
("I", "i"),
58+
("I", "ı"),
59+
("i", "I"),
60+
("i", "İ"),
61+
("i", "ı"),
62+
("İ", "I"),
63+
("İ", "i"),
64+
("İ", "ı"),
65+
("ı", "I"),
66+
("ı", "i"),
67+
("ı", "İ"),
68+
("ä", "Ä"),
69+
("ß", "SS"),
70+
("Ä", "Ö"),
71+
("Ä", "Ö"),
72+
("I", "İ"),
73+
("I", "i"),
74+
("I", "ı"),
75+
("i", "I"),
76+
("i", "İ"),
77+
("i", "ı"),
78+
("İ", "I"),
79+
("İ", "i"),
80+
("İ", "ı"),
81+
("ı", "I"),
82+
("ı", "i"),
83+
("ı", "İ"),
84+
];
85+
// Test that `cmd.env` matches `env::set_var` when setting two strings that
86+
// may (or may not) be case-folded when compared.
87+
for (a, b) in test_cases.iter() {
88+
let mut cmd = Command::new("cmd");
89+
cmd.env(a, "1");
90+
cmd.env(b, "2");
91+
env::set_var(a, "1");
92+
env::set_var(b, "2");
93+
94+
for (key, value) in cmd.get_envs() {
95+
assert_eq!(
96+
env::var(key).ok(),
97+
value.map(|s| s.to_string_lossy().into_owned()),
98+
"command environment mismatch: {} {}",
99+
a,
100+
b
101+
);
102+
}
103+
}
104+
}

0 commit comments

Comments
 (0)