|
4 | 4 | mod tests;
|
5 | 5 |
|
6 | 6 | use crate::borrow::Borrow;
|
| 7 | +use crate::cmp; |
7 | 8 | use crate::collections::BTreeMap;
|
8 | 9 | use crate::convert::{TryFrom, TryInto};
|
9 | 10 | use crate::env;
|
@@ -34,32 +35,95 @@ use libc::{c_void, EXIT_FAILURE, EXIT_SUCCESS};
|
34 | 35 | // Command
|
35 | 36 | ////////////////////////////////////////////////////////////////////////////////
|
36 | 37 |
|
37 |
| -#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] |
| 38 | +#[derive(Clone, Debug, Eq)] |
38 | 39 | #[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 | +} |
40 | 103 |
|
| 104 | +// Environment variable keys should preserve their original case even though |
| 105 | +// they are compared using a caseless string mapping. |
41 | 106 | 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 } |
45 | 109 | }
|
46 | 110 | }
|
47 | 111 |
|
48 | 112 | impl From<EnvKey> for OsString {
|
49 | 113 | fn from(k: EnvKey) -> Self {
|
50 |
| - k.0 |
| 114 | + k.os_string |
51 | 115 | }
|
52 | 116 | }
|
53 | 117 |
|
54 | 118 | impl Borrow<OsStr> for EnvKey {
|
55 | 119 | fn borrow(&self) -> &OsStr {
|
56 |
| - &self.0 |
| 120 | + &self.os_string |
57 | 121 | }
|
58 | 122 | }
|
59 | 123 |
|
60 | 124 | impl AsRef<OsStr> for EnvKey {
|
61 | 125 | fn as_ref(&self) -> &OsStr {
|
62 |
| - &self.0 |
| 126 | + &self.os_string |
63 | 127 | }
|
64 | 128 | }
|
65 | 129 |
|
@@ -537,7 +601,8 @@ fn make_envp(maybe_env: Option<BTreeMap<EnvKey, OsString>>) -> io::Result<(*mut
|
537 | 601 | }
|
538 | 602 |
|
539 | 603 | for (k, v) in env {
|
540 |
| - blk.extend(ensure_no_nuls(k.0)?.encode_wide()); |
| 604 | + ensure_no_nuls(k.os_string)?; |
| 605 | + blk.extend(k.utf16); |
541 | 606 | blk.push('=' as u16);
|
542 | 607 | blk.extend(ensure_no_nuls(v)?.encode_wide());
|
543 | 608 | blk.push(0);
|
|
0 commit comments