Skip to content

Commit f3f9512

Browse files
authored
Remove the default collected output buffer limit and throw an error when the limit is reached (#130)
1 parent 05c808d commit f3f9512

File tree

10 files changed

+151
-119
lines changed

10 files changed

+151
-119
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ let result = try await run(.path("/bin/exe"), platformOptions: platformOptions)
143143

144144
By default, `Subprocess`:
145145
- Doesn’t send any input to the child process’s standard input
146-
- Captures the child process’s standard output as a `String`, up to 128kB
146+
- Asks the user how to capture the output
147147
- Ignores the child process’s standard error
148148

149149
You can tailor how `Subprocess` handles the standard input, standard output, and standard error by setting the `input`, `output`, and `error` parameters:
@@ -222,13 +222,13 @@ Use it by setting `.fileDescriptor(closeAfterSpawningProcess:)` for `output` or
222222

223223
This option collects output as a `String` with the given encoding.
224224

225-
Use it by setting `.string` or `.string(limit:encoding:)` for `output` or `error`.
225+
Use it by setting `.string(limit:encoding:)` for `output` or `error`.
226226

227227
#### `BytesOutput`
228228

229229
This option collects output as `[UInt8]`.
230230

231-
Use it by setting `.bytes` or `.bytes(limit:)` for `output` or `error`.
231+
Use it by setting`.bytes(limit:)` for `output` or `error`.
232232

233233

234234
### Cross-platform support

Sources/Subprocess/API.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public func run<
4141
workingDirectory: FilePath? = nil,
4242
platformOptions: PlatformOptions = PlatformOptions(),
4343
input: Input = .none,
44-
output: Output = .string,
44+
output: Output,
4545
error: Error = .discarded
4646
) async throws -> CollectedResult<Output, Error> {
4747
let configuration = Configuration(
@@ -84,7 +84,7 @@ public func run<
8484
workingDirectory: FilePath? = nil,
8585
platformOptions: PlatformOptions = PlatformOptions(),
8686
input: borrowing Span<InputElement>,
87-
output: Output = .string,
87+
output: Output,
8888
error: Error = .discarded
8989
) async throws -> CollectedResult<Output, Error> {
9090
typealias RunResult = (
@@ -483,7 +483,7 @@ public func run<
483483
>(
484484
_ configuration: Configuration,
485485
input: Input = .none,
486-
output: Output = .string,
486+
output: Output,
487487
error: Error = .discarded
488488
) async throws -> CollectedResult<Output, Error> {
489489
typealias RunResult = (

Sources/Subprocess/Error.swift

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ extension SubprocessError {
4242
case failedToMonitorProcess
4343
case streamOutputExceedsLimit(Int)
4444
case asyncIOFailed(String)
45+
case outputBufferLimitExceeded(Int)
4546
// Signal
4647
case failedToSendSignal(Int32)
4748
// Windows Only
@@ -70,18 +71,20 @@ extension SubprocessError {
7071
return 6
7172
case .asyncIOFailed(_):
7273
return 7
73-
case .failedToSendSignal(_):
74+
case .outputBufferLimitExceeded(_):
7475
return 8
75-
case .failedToTerminate:
76+
case .failedToSendSignal(_):
7677
return 9
77-
case .failedToSuspend:
78+
case .failedToTerminate:
7879
return 10
79-
case .failedToResume:
80+
case .failedToSuspend:
8081
return 11
81-
case .failedToCreatePipe:
82+
case .failedToResume:
8283
return 12
83-
case .invalidWindowsPath(_):
84+
case .failedToCreatePipe:
8485
return 13
86+
case .invalidWindowsPath(_):
87+
return 14
8588
}
8689
}
8790

@@ -113,6 +116,8 @@ extension SubprocessError: CustomStringConvertible, CustomDebugStringConvertible
113116
return "Failed to create output from current buffer because the output limit (\(limit)) was reached."
114117
case .asyncIOFailed(let reason):
115118
return "An error occurred within the AsyncIO subsystem: \(reason). Underlying error: \(self.underlyingError!)"
119+
case .outputBufferLimitExceeded(let limit):
120+
return "Output exceeds the limit of \(limit) bytes."
116121
case .failedToSendSignal(let signal):
117122
return "Failed to send signal \(signal) to the child process."
118123
case .failedToTerminate:

Sources/Subprocess/IO/Output.swift

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -158,13 +158,25 @@ public struct BytesOutput: OutputProtocol {
158158
var result: [UInt8]? = nil
159159
#endif
160160
do {
161-
result = try await AsyncIO.shared.read(from: diskIO, upTo: self.maxSize)
161+
var maxLength = self.maxSize
162+
if maxLength != .max {
163+
// If we actually have a max length, attempt to read one
164+
// more byte to determine whether output exceeds the limit
165+
maxLength += 1
166+
}
167+
result = try await AsyncIO.shared.read(from: diskIO, upTo: maxLength)
162168
} catch {
163169
try diskIO.safelyClose()
164170
throw error
165171
}
166-
167172
try diskIO.safelyClose()
173+
174+
if let result, result.count > self.maxSize {
175+
throw SubprocessError(
176+
code: .init(.outputBufferLimitExceeded(self.maxSize)),
177+
underlyingError: nil
178+
)
179+
}
168180
#if canImport(Darwin)
169181
return result?.array() ?? []
170182
#else
@@ -213,16 +225,19 @@ extension OutputProtocol where Self == FileDescriptorOutput {
213225
}
214226

215227
extension OutputProtocol where Self == StringOutput<UTF8> {
216-
/// Create a `Subprocess` output that collects output as
217-
/// UTF8 String with 128kb limit.
218-
public static var string: Self {
219-
.init(limit: 128 * 1024, encoding: UTF8.self)
228+
/// Create a `Subprocess` output that collects output as UTF8 String
229+
/// with a buffer limit in bytes. Subprocess throws an error if the
230+
/// child process emits more bytes than the limit.
231+
public static func string(limit: Int) -> Self {
232+
return .init(limit: limit, encoding: UTF8.self)
220233
}
221234
}
222235

223236
extension OutputProtocol {
224237
/// Create a `Subprocess` output that collects output as
225-
/// `String` using the given encoding up to limit it bytes.
238+
/// `String` using the given encoding up to limit in bytes.
239+
/// Subprocess throws an error if the child process emits
240+
/// more bytes than the limit.
226241
public static func string<Encoding: Unicode.Encoding>(
227242
limit: Int,
228243
encoding: Encoding.Type
@@ -234,11 +249,8 @@ extension OutputProtocol {
234249

235250
extension OutputProtocol where Self == BytesOutput {
236251
/// Create a `Subprocess` output that collects output as
237-
/// `Buffer` with 128kb limit.
238-
public static var bytes: Self { .init(limit: 128 * 1024) }
239-
240-
/// Create a `Subprocess` output that collects output as
241-
/// `Buffer` up to limit it bytes.
252+
/// `Buffer` with a buffer limit in bytes. Subprocess throws
253+
/// an error if the child process emits more bytes than the limit.
242254
public static func bytes(limit: Int) -> Self {
243255
return .init(limit: limit)
244256
}
@@ -299,13 +311,25 @@ extension OutputProtocol {
299311
var result: [UInt8]? = nil
300312
#endif
301313
do {
302-
result = try await AsyncIO.shared.read(from: diskIO, upTo: self.maxSize)
314+
var maxLength = self.maxSize
315+
if maxLength != .max {
316+
// If we actually have a max length, attempt to read one
317+
// more byte to determine whether output exceeds the limit
318+
maxLength += 1
319+
}
320+
result = try await AsyncIO.shared.read(from: diskIO, upTo: maxLength)
303321
} catch {
304322
try diskIO.safelyClose()
305323
throw error
306324
}
307325

308326
try diskIO.safelyClose()
327+
if let result, result.count > self.maxSize {
328+
throw SubprocessError(
329+
code: .init(.outputBufferLimitExceeded(self.maxSize)),
330+
underlyingError: nil
331+
)
332+
}
309333
#if canImport(Darwin)
310334
return try self.output(from: result ?? .empty)
311335
#else

Sources/Subprocess/SubprocessFoundation/Output+Foundation.swift

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,8 @@ public struct DataOutput: OutputProtocol {
4343

4444
extension OutputProtocol where Self == DataOutput {
4545
/// Create a `Subprocess` output that collects output as `Data`
46-
/// up to 128kb.
47-
public static var data: Self {
48-
return .data(limit: 128 * 1024)
49-
}
50-
51-
/// Create a `Subprocess` output that collects output as `Data`
52-
/// with given max number of bytes to collect.
46+
/// with given buffer limit in bytes. Subprocess throws an error
47+
/// if the child process emits more bytes than the limit.
5348
public static func data(limit: Int) -> Self {
5449
return .init(limit: limit)
5550
}

Tests/SubprocessTests/SubprocessTests+Darwin.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ struct SubprocessDarwinTests {
4141
.path("/bin/bash"),
4242
arguments: ["-c", "ps -o pid,pgid,tpgid -p $$"],
4343
platformOptions: platformOptions,
44-
output: .string
44+
output: .string(limit: .max)
4545
)
4646
try assertNewSessionCreated(with: psResult)
4747
}
@@ -58,7 +58,7 @@ struct SubprocessDarwinTests {
5858
let pwdResult = try await Subprocess.run(
5959
.path("/bin/pwd"),
6060
platformOptions: platformOptions,
61-
output: .string
61+
output: .string(limit: .max)
6262
)
6363
#expect(pwdResult.terminationStatus.isSuccess)
6464
let currentDir = try #require(

Tests/SubprocessTests/SubprocessTests+Linting.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ struct SubprocessLintingTest {
7272
let lintResult = try await Subprocess.run(
7373
configuration,
7474
output: .discarded,
75-
error: .string
75+
error: .string(limit: .max)
7676
)
7777
#expect(
7878
lintResult.terminationStatus.isSuccess,

Tests/SubprocessTests/SubprocessTests+Linux.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ struct SubprocessLinuxTests {
5050
.path("/usr/bin/id"),
5151
arguments: ["-g"],
5252
platformOptions: platformOptions,
53-
output: .string,
54-
error: .string
53+
output: .string(limit: 32),
54+
error: .string(limit: 32)
5555
)
5656
let error = try #require(idResult.standardError)
5757
try #require(error == "")

0 commit comments

Comments
 (0)