Skip to content

Commit 6097a92

Browse files
borsgitbot
authored and
gitbot
committed
Auto merge of rust-lang#85270 - ChrisDenton:win-env-case, r=m-ou-se
When using `process::Command` on Windows, environment variable names must be case-preserving but case-insensitive When using `Command` to set the environment variables, the key should be compared as uppercase Unicode but when set it should preserve the original case. Fixes rust-lang#85242
2 parents 2870d48 + 9a6d8bb commit 6097a92

File tree

3 files changed

+147
-9
lines changed

3 files changed

+147
-9
lines changed

Diff for: std/src/sys/windows/c.rs

+12
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ pub type ADDRESS_FAMILY = USHORT;
7272
pub const TRUE: BOOL = 1;
7373
pub const FALSE: BOOL = 0;
7474

75+
pub const CSTR_LESS_THAN: c_int = 1;
76+
pub const CSTR_EQUAL: c_int = 2;
77+
pub const CSTR_GREATER_THAN: c_int = 3;
78+
7579
pub const FILE_ATTRIBUTE_READONLY: DWORD = 0x1;
7680
pub const FILE_ATTRIBUTE_DIRECTORY: DWORD = 0x10;
7781
pub const FILE_ATTRIBUTE_REPARSE_POINT: DWORD = 0x400;
@@ -952,6 +956,14 @@ extern "system" {
952956
pub fn ReleaseSRWLockShared(SRWLock: PSRWLOCK);
953957
pub fn TryAcquireSRWLockExclusive(SRWLock: PSRWLOCK) -> BOOLEAN;
954958
pub fn TryAcquireSRWLockShared(SRWLock: PSRWLOCK) -> BOOLEAN;
959+
960+
pub fn CompareStringOrdinal(
961+
lpString1: LPCWSTR,
962+
cchCount1: c_int,
963+
lpString2: LPCWSTR,
964+
cchCount2: c_int,
965+
bIgnoreCase: BOOL,
966+
) -> c_int;
955967
}
956968

957969
#[link(name = "ws2_32")]

Diff for: std/src/sys/windows/process.rs

+74-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,95 @@ 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+
// Comparing Windows environment variable keys[1] are behaviourally the
50+
// composition of two operations[2]:
51+
//
52+
// 1. Case-fold both strings. This is done using a language-independent
53+
// uppercase mapping that's unique to Windows (albeit based on data from an
54+
// older Unicode spec). It only operates on individual UTF-16 code units so
55+
// surrogates are left unchanged. This uppercase mapping can potentially change
56+
// between Windows versions.
57+
//
58+
// 2. Perform an ordinal comparison of the strings. A comparison using ordinal
59+
// is just a comparison based on the numerical value of each UTF-16 code unit[3].
60+
//
61+
// Because the case-folding mapping is unique to Windows and not guaranteed to
62+
// be stable, we ask the OS to compare the strings for us. This is done by
63+
// calling `CompareStringOrdinal`[4] with `bIgnoreCase` set to `TRUE`.
64+
//
65+
// [1] https://docs.microsoft.com/en-us/dotnet/standard/base-types/best-practices-strings#choosing-a-stringcomparison-member-for-your-method-call
66+
// [2] https://docs.microsoft.com/en-us/dotnet/standard/base-types/best-practices-strings#stringtoupper-and-stringtolower
67+
// [3] https://docs.microsoft.com/en-us/dotnet/api/system.stringcomparison?view=net-5.0#System_StringComparison_Ordinal
68+
// [4] https://docs.microsoft.com/en-us/windows/win32/api/stringapiset/nf-stringapiset-comparestringordinal
69+
impl Ord for EnvKey {
70+
fn cmp(&self, other: &Self) -> cmp::Ordering {
71+
unsafe {
72+
let result = c::CompareStringOrdinal(
73+
self.utf16.as_ptr(),
74+
self.utf16.len() as _,
75+
other.utf16.as_ptr(),
76+
other.utf16.len() as _,
77+
c::TRUE,
78+
);
79+
match result {
80+
c::CSTR_LESS_THAN => cmp::Ordering::Less,
81+
c::CSTR_EQUAL => cmp::Ordering::Equal,
82+
c::CSTR_GREATER_THAN => cmp::Ordering::Greater,
83+
// `CompareStringOrdinal` should never fail so long as the parameters are correct.
84+
_ => panic!("comparing environment keys failed: {}", Error::last_os_error()),
85+
}
86+
}
87+
}
88+
}
89+
impl PartialOrd for EnvKey {
90+
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
91+
Some(self.cmp(other))
92+
}
93+
}
94+
impl PartialEq for EnvKey {
95+
fn eq(&self, other: &Self) -> bool {
96+
if self.utf16.len() != other.utf16.len() {
97+
false
98+
} else {
99+
self.cmp(other) == cmp::Ordering::Equal
100+
}
101+
}
102+
}
40103

104+
// Environment variable keys should preserve their original case even though
105+
// they are compared using a caseless string mapping.
41106
impl From<OsString> for EnvKey {
42-
fn from(mut k: OsString) -> Self {
43-
k.make_ascii_uppercase();
44-
EnvKey(k)
107+
fn from(k: OsString) -> Self {
108+
EnvKey { utf16: k.encode_wide().collect(), os_string: k }
45109
}
46110
}
47111

48112
impl From<EnvKey> for OsString {
49113
fn from(k: EnvKey) -> Self {
50-
k.0
114+
k.os_string
51115
}
52116
}
53117

54118
impl Borrow<OsStr> for EnvKey {
55119
fn borrow(&self) -> &OsStr {
56-
&self.0
120+
&self.os_string
57121
}
58122
}
59123

60124
impl AsRef<OsStr> for EnvKey {
61125
fn as_ref(&self) -> &OsStr {
62-
&self.0
126+
&self.os_string
63127
}
64128
}
65129

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

533597
for (k, v) in env {
534-
blk.extend(ensure_no_nuls(k.0)?.encode_wide());
598+
ensure_no_nuls(k.os_string)?;
599+
blk.extend(k.utf16);
535600
blk.push('=' as u16);
536601
blk.extend(ensure_no_nuls(v)?.encode_wide());
537602
blk.push(0);

Diff for: 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)