@@ -13,13 +13,15 @@ enum PasswordSourceResolver {
1313 private static let logger = Logger ( subsystem: " com.TablePro " , category: " PasswordSourceResolver " )
1414
1515 private static let commandTimeoutSeconds : UInt64 = 30
16+ private static let maxOutputBytes = 1_048_576
1617
1718 enum ResolutionError : LocalizedError {
1819 case fileNotFound( path: String )
1920 case fileUnreadable( path: String )
2021 case environmentVariableNotSet( name: String )
2122 case commandFailed( exitCode: Int32 , stderr: String )
2223 case commandTimedOut
24+ case outputTooLarge
2325 case emptyPassword
2426
2527 var errorDescription : String ? {
@@ -45,6 +47,8 @@ enum PasswordSourceResolver {
4547 return String ( format: String ( localized: " Password command failed (exit %d): %@ " ) , exitCode, message)
4648 case . commandTimedOut:
4749 return String ( localized: " Password command timed out after 30 seconds " )
50+ case . outputTooLarge:
51+ return String ( localized: " Password command produced too much output " )
4852 case . emptyPassword:
4953 return String ( localized: " The password source produced an empty password " )
5054 }
@@ -94,11 +98,15 @@ enum PasswordSourceResolver {
9498 process. standardOutput = stdoutPipe
9599 process. standardError = stderrPipe
96100
97- let stdoutCollector = PipeDataCollector ( )
98- let stderrCollector = PipeDataCollector ( )
101+ let stdoutCollector = PipeDataCollector ( maxBytes : maxOutputBytes )
102+ let stderrCollector = PipeDataCollector ( maxBytes : maxOutputBytes )
99103 stdoutPipe. fileHandleForReading. readabilityHandler = { handle in
100104 let chunk = handle. availableData
101- if !chunk. isEmpty { stdoutCollector. append ( chunk) }
105+ guard !chunk. isEmpty else { return }
106+ stdoutCollector. append ( chunk)
107+ if stdoutCollector. overflowed, process. isRunning {
108+ process. terminate ( )
109+ }
102110 }
103111 stderrPipe. fileHandleForReading. readabilityHandler = { handle in
104112 let chunk = handle. availableData
@@ -107,9 +115,13 @@ enum PasswordSourceResolver {
107115
108116 try process. run ( )
109117
118+ let didTimeout = AtomicFlag ( )
110119 let timeoutTask = Task . detached {
111120 try await Task . sleep ( nanoseconds: timeoutSeconds * 1_000_000_000 )
112- if process. isRunning { process. terminate ( ) }
121+ if process. isRunning {
122+ didTimeout. set ( )
123+ process. terminate ( )
124+ }
113125 }
114126
115127 process. waitUntilExit ( )
@@ -118,7 +130,10 @@ enum PasswordSourceResolver {
118130 stdoutPipe. fileHandleForReading. readabilityHandler = nil
119131 stderrPipe. fileHandleForReading. readabilityHandler = nil
120132
121- if process. terminationReason == . uncaughtSignal {
133+ if stdoutCollector. overflowed {
134+ throw ResolutionError . outputTooLarge
135+ }
136+ if didTimeout. isSet {
122137 throw ResolutionError . commandTimedOut
123138 }
124139 if process. terminationStatus != 0 {
@@ -167,12 +182,34 @@ enum PasswordSourceResolver {
167182
168183private final class PipeDataCollector : @unchecked Sendable {
169184 private let lock = NSLock ( )
185+ private let maxBytes : Int
170186 private var data = Data ( )
187+ private var didOverflow = false
188+
189+ init ( maxBytes: Int ) {
190+ self . maxBytes = maxBytes
191+ }
171192
172193 func append( _ chunk: Data ) {
173194 lock. lock ( )
174- data. append ( chunk)
175- lock. unlock ( )
195+ defer { lock. unlock ( ) }
196+ let remaining = maxBytes - data. count
197+ guard remaining > 0 else {
198+ didOverflow = true
199+ return
200+ }
201+ if chunk. count > remaining {
202+ data. append ( chunk. prefix ( remaining) )
203+ didOverflow = true
204+ } else {
205+ data. append ( chunk)
206+ }
207+ }
208+
209+ var overflowed : Bool {
210+ lock. lock ( )
211+ defer { lock. unlock ( ) }
212+ return didOverflow
176213 }
177214
178215 var string : String {
@@ -181,3 +218,20 @@ private final class PipeDataCollector: @unchecked Sendable {
181218 return String ( data: data, encoding: . utf8) ?? " "
182219 }
183220}
221+
222+ private final class AtomicFlag : @unchecked Sendable {
223+ private let lock = NSLock ( )
224+ private var value = false
225+
226+ func set( ) {
227+ lock. lock ( )
228+ value = true
229+ lock. unlock ( )
230+ }
231+
232+ var isSet : Bool {
233+ lock. lock ( )
234+ defer { lock. unlock ( ) }
235+ return value
236+ }
237+ }
0 commit comments