Skip to content

Commit 4a5af15

Browse files
committed
Make Environment keys case-insensitive on Windows
This is how the platform treats them, and helps avoid issues where indexing into an Environment dictionary with the wrong casing fails to return a value. Closes #134
1 parent 6eb91f6 commit 4a5af15

File tree

3 files changed

+93
-28
lines changed

3 files changed

+93
-28
lines changed

Sources/Subprocess/Configuration.swift

Lines changed: 81 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -361,8 +361,8 @@ extension Arguments: CustomStringConvertible, CustomDebugStringConvertible {
361361
/// A set of environment variables to use when executing the subprocess.
362362
public struct Environment: Sendable, Hashable {
363363
internal enum Configuration: Sendable, Hashable {
364-
case inherit([String: String])
365-
case custom([String: String])
364+
case inherit([EnvironmentKey: String])
365+
case custom([EnvironmentKey: String])
366366
#if !os(Windows)
367367
case rawBytes([[UInt8]])
368368
#endif
@@ -379,11 +379,11 @@ public struct Environment: Sendable, Hashable {
379379
return .init(config: .inherit([:]))
380380
}
381381
/// Override the provided `newValue` in the existing `Environment`
382-
public func updating(_ newValue: [String: String]) -> Self {
382+
public func updating(_ newValue: [EnvironmentKey: String]) -> Self {
383383
return .init(config: .inherit(newValue))
384384
}
385385
/// Use custom environment variables
386-
public static func custom(_ newValue: [String: String]) -> Self {
386+
public static func custom(_ newValue: [EnvironmentKey: String]) -> Self {
387387
return .init(config: .custom(newValue))
388388
}
389389

@@ -424,9 +424,9 @@ extension Environment: CustomStringConvertible, CustomDebugStringConvertible {
424424
return self.description
425425
}
426426

427-
internal static func currentEnvironmentValues() -> [String: String] {
427+
internal static func currentEnvironmentValues() -> [EnvironmentKey: String] {
428428
return self.withCopiedEnv { environments in
429-
var results: [String: String] = [:]
429+
var results: [EnvironmentKey: String] = [:]
430430
for env in environments {
431431
let environmentString = String(cString: env)
432432

@@ -448,13 +448,87 @@ extension Environment: CustomStringConvertible, CustomDebugStringConvertible {
448448
let value = String(
449449
environmentString[environmentString.index(after: delimiter)..<environmentString.endIndex]
450450
)
451-
results[key] = value
451+
results[EnvironmentKey(key)] = value
452452
}
453453
return results
454454
}
455455
}
456456
}
457457

458+
/// A key used to access values in an ``Environment``.
459+
///
460+
/// This type respects the compiled platform's case sensitivity requirements.
461+
public struct EnvironmentKey {
462+
public var rawValue: String
463+
464+
package init(_ rawValue: String) {
465+
self.rawValue = rawValue
466+
}
467+
}
468+
469+
extension EnvironmentKey {
470+
package static let path: Self = "PATH"
471+
}
472+
473+
extension EnvironmentKey: CodingKeyRepresentable {}
474+
475+
extension EnvironmentKey: Comparable {
476+
public static func < (lhs: Self, rhs: Self) -> Bool {
477+
// Even on windows use a stable sort order.
478+
lhs.rawValue < rhs.rawValue
479+
}
480+
}
481+
482+
extension EnvironmentKey: CustomStringConvertible {
483+
public var description: String { self.rawValue }
484+
}
485+
486+
extension EnvironmentKey: Encodable {
487+
public func encode(to encoder: any Swift.Encoder) throws {
488+
try self.rawValue.encode(to: encoder)
489+
}
490+
}
491+
492+
extension EnvironmentKey: Equatable {
493+
public static func == (_ lhs: Self, _ rhs: Self) -> Bool {
494+
#if os(Windows)
495+
lhs.rawValue.lowercased() == rhs.rawValue.lowercased()
496+
#else
497+
lhs.rawValue == rhs.rawValue
498+
#endif
499+
}
500+
}
501+
502+
extension EnvironmentKey: ExpressibleByStringLiteral {
503+
public init(stringLiteral rawValue: String) {
504+
self.init(rawValue)
505+
}
506+
}
507+
508+
extension EnvironmentKey: Decodable {
509+
public init(from decoder: any Swift.Decoder) throws {
510+
self.rawValue = try String(from: decoder)
511+
}
512+
}
513+
514+
extension EnvironmentKey: Hashable {
515+
public func hash(into hasher: inout Hasher) {
516+
#if os(Windows)
517+
self.rawValue.lowercased().hash(into: &hasher)
518+
#else
519+
self.rawValue.hash(into: &hasher)
520+
#endif
521+
}
522+
}
523+
524+
extension EnvironmentKey: RawRepresentable {
525+
public init?(rawValue: String) {
526+
self.rawValue = rawValue
527+
}
528+
}
529+
530+
extension EnvironmentKey: Sendable {}
531+
458532
// MARK: - TerminationStatus
459533

460534
/// An exit status of a subprocess.

Sources/Subprocess/Platforms/Subprocess+Unix.swift

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -145,24 +145,22 @@ extension Execution {
145145

146146
// MARK: - Environment Resolution
147147
extension Environment {
148-
internal static let pathVariableName = "PATH"
149-
150148
internal func pathValue() -> String? {
151149
switch self.config {
152150
case .inherit(let overrides):
153151
// If PATH value exists in overrides, use it
154-
if let value = overrides[Self.pathVariableName] {
152+
if let value = overrides[.path] {
155153
return value
156154
}
157155
// Fall back to current process
158-
return Self.currentEnvironmentValues()[Self.pathVariableName]
156+
return Self.currentEnvironmentValues()[.path]
159157
case .custom(let fullEnvironment):
160-
if let value = fullEnvironment[Self.pathVariableName] {
158+
if let value = fullEnvironment[.path] {
161159
return value
162160
}
163161
return nil
164162
case .rawBytes(let rawBytesArray):
165-
let needle: [UInt8] = Array("\(Self.pathVariableName)=".utf8)
163+
let needle: [UInt8] = Array("\(EnvironmentKey.path.rawValue)=".utf8)
166164
for row in rawBytesArray {
167165
guard row.starts(with: needle) else {
168166
continue

Sources/Subprocess/Platforms/Subprocess+Windows.swift

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -926,13 +926,13 @@ extension Environment {
926926
switch self.config {
927927
case .inherit(let overrides):
928928
// If PATH value exists in overrides, use it
929-
if let value = overrides.pathValue() {
929+
if let value = overrides[.path] {
930930
return value
931931
}
932932
// Fall back to current process
933-
return Self.currentEnvironmentValues().pathValue()
933+
return Self.currentEnvironmentValues()[.path]
934934
case .custom(let fullEnvironment):
935-
if let value = fullEnvironment.pathValue() {
935+
if let value = fullEnvironment[.path] {
936936
return value
937937
}
938938
return nil
@@ -1006,7 +1006,7 @@ extension Configuration {
10061006
intendedWorkingDir: String?
10071007
) {
10081008
// Prepare environment
1009-
var env: [String: String] = [:]
1009+
var env: [EnvironmentKey: String] = [:]
10101010
switch self.environment.config {
10111011
case .custom(let customValues):
10121012
// Use the custom values directly
@@ -1020,17 +1020,17 @@ extension Configuration {
10201020
}
10211021
// On Windows, the PATH is required in order to locate dlls needed by
10221022
// the process so we should also pass that to the child
1023-
if env.pathValue() == nil,
1024-
let parentPath = Environment.currentEnvironmentValues().pathValue()
1023+
if env[.path] == nil,
1024+
let parentPath = Environment.currentEnvironmentValues()[.path]
10251025
{
1026-
env["Path"] = parentPath
1026+
env[.path] = parentPath
10271027
}
10281028
// The environment string must be terminated by a double
10291029
// null-terminator. Otherwise, CreateProcess will fail with
10301030
// INVALID_PARMETER.
10311031
let environmentString =
10321032
env.map {
1033-
$0.key + "=" + $0.value
1033+
$0.key.rawValue + "=" + $0.value
10341034
}.joined(separator: "\0") + "\0\0"
10351035

10361036
// Prepare arguments
@@ -1509,11 +1509,4 @@ internal func fillNullTerminatedWideStringBuffer(
15091509
throw SubprocessError.UnderlyingError(rawValue: DWORD(ERROR_INSUFFICIENT_BUFFER))
15101510
}
15111511

1512-
// Windows environment key is case insensitive
1513-
extension Dictionary where Key == String, Value == String {
1514-
internal func pathValue() -> String? {
1515-
return self["Path"] ?? self["PATH"] ?? self["path"]
1516-
}
1517-
}
1518-
15191512
#endif // canImport(WinSDK)

0 commit comments

Comments
 (0)