Skip to content

Commit 25357a8

Browse files
Make Environment keys case-insensitive on Windows (#174)
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 b778595 commit 25357a8

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
@@ -401,8 +401,8 @@ extension Arguments: CustomStringConvertible, CustomDebugStringConvertible {
401401
/// A set of environment variables to use when executing the subprocess.
402402
public struct Environment: Sendable, Hashable {
403403
internal enum Configuration: Sendable, Hashable {
404-
case inherit([String: String])
405-
case custom([String: String])
404+
case inherit([Key: String])
405+
case custom([Key: String])
406406
#if !os(Windows)
407407
case rawBytes([[UInt8]])
408408
#endif
@@ -419,11 +419,11 @@ public struct Environment: Sendable, Hashable {
419419
return .init(config: .inherit([:]))
420420
}
421421
/// Override the provided `newValue` in the existing `Environment`
422-
public func updating(_ newValue: [String: String]) -> Self {
422+
public func updating(_ newValue: [Key: String]) -> Self {
423423
return .init(config: .inherit(newValue))
424424
}
425425
/// Use custom environment variables
426-
public static func custom(_ newValue: [String: String]) -> Self {
426+
public static func custom(_ newValue: [Key: String]) -> Self {
427427
return .init(config: .custom(newValue))
428428
}
429429

@@ -436,6 +436,17 @@ public struct Environment: Sendable, Hashable {
436436
}
437437

438438
extension Environment: CustomStringConvertible, CustomDebugStringConvertible {
439+
/// A key used to access values in an ``Environment``.
440+
///
441+
/// This type respects the compiled platform's case sensitivity requirements.
442+
public struct Key {
443+
public var rawValue: String
444+
445+
package init(_ rawValue: String) {
446+
self.rawValue = rawValue
447+
}
448+
}
449+
439450
/// A textual representation of the environment.
440451
public var description: String {
441452
switch self.config {
@@ -464,9 +475,9 @@ extension Environment: CustomStringConvertible, CustomDebugStringConvertible {
464475
return self.description
465476
}
466477

467-
internal static func currentEnvironmentValues() -> [String: String] {
478+
internal static func currentEnvironmentValues() -> [Key: String] {
468479
return self.withCopiedEnv { environments in
469-
var results: [String: String] = [:]
480+
var results: [Key: String] = [:]
470481
for env in environments {
471482
let environmentString = String(cString: env)
472483

@@ -488,13 +499,76 @@ extension Environment: CustomStringConvertible, CustomDebugStringConvertible {
488499
let value = String(
489500
environmentString[environmentString.index(after: delimiter)..<environmentString.endIndex]
490501
)
491-
results[key] = value
502+
results[Key(key)] = value
492503
}
493504
return results
494505
}
495506
}
496507
}
497508

509+
extension Environment.Key {
510+
package static let path: Self = "PATH"
511+
}
512+
513+
extension Environment.Key: CodingKeyRepresentable {}
514+
515+
extension Environment.Key: Comparable {
516+
public static func < (lhs: Self, rhs: Self) -> Bool {
517+
// Even on windows use a stable sort order.
518+
lhs.rawValue < rhs.rawValue
519+
}
520+
}
521+
522+
extension Environment.Key: CustomStringConvertible {
523+
public var description: String { self.rawValue }
524+
}
525+
526+
extension Environment.Key: Encodable {
527+
public func encode(to encoder: any Swift.Encoder) throws {
528+
try self.rawValue.encode(to: encoder)
529+
}
530+
}
531+
532+
extension Environment.Key: Equatable {
533+
public static func == (_ lhs: Self, _ rhs: Self) -> Bool {
534+
#if os(Windows)
535+
lhs.rawValue.lowercased() == rhs.rawValue.lowercased()
536+
#else
537+
lhs.rawValue == rhs.rawValue
538+
#endif
539+
}
540+
}
541+
542+
extension Environment.Key: ExpressibleByStringLiteral {
543+
public init(stringLiteral rawValue: String) {
544+
self.init(rawValue)
545+
}
546+
}
547+
548+
extension Environment.Key: Decodable {
549+
public init(from decoder: any Swift.Decoder) throws {
550+
self.rawValue = try String(from: decoder)
551+
}
552+
}
553+
554+
extension Environment.Key: Hashable {
555+
public func hash(into hasher: inout Hasher) {
556+
#if os(Windows)
557+
self.rawValue.lowercased().hash(into: &hasher)
558+
#else
559+
self.rawValue.hash(into: &hasher)
560+
#endif
561+
}
562+
}
563+
564+
extension Environment.Key: RawRepresentable {
565+
public init?(rawValue: String) {
566+
self.rawValue = rawValue
567+
}
568+
}
569+
570+
extension Environment.Key: Sendable {}
571+
498572
// MARK: - TerminationStatus
499573

500574
/// 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("\(Key.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: [Environment.Key: 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)