Skip to content

Commit c7cba01

Browse files
FileHelpers: Add FileDescriptor.read(filling buffer:)
Because `read(into:)` and `write(into:)` return the number of bytes read or written which may be smaller than desired, users will often want to call these functions in a loop until they have read or written all the bytes they require. Such loops require keeping track of the index and will be repeated toil in each application. Swift System already provides an extensions `writeAll(_:)` and `writeAll(toAbsoluteOffset:_:)` which operates on a sequence of bytes and writes all the bytes in the sequence. This patch adds an analogous helper function for reading, `read(filling buffer:)` which takes an `UnsafeMutableRawBufferPointer` and reads until the buffer is full, or until EOF is reached.
1 parent a77a331 commit c7cba01

File tree

3 files changed

+111
-2
lines changed

3 files changed

+111
-2
lines changed

Sources/System/FileHelpers.swift

+69
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,75 @@ extension FileDescriptor {
7979
}
8080
}
8181

82+
/// Reads bytes at the current file offset into a buffer until the buffer is filled.
83+
///
84+
/// - Parameters:
85+
/// - buffer: The region of memory to read into.
86+
/// - Returns: The number of bytes that were read, equal to `buffer.count`.
87+
///
88+
/// This method either reads until `buffer` is full, or throws an error if
89+
/// only part of the buffer was filled.
90+
///
91+
/// The <doc://com.apple.documentation/documentation/swift/unsafemutablerawbufferpointer/3019191-count> property of `buffer`
92+
/// determines the number of bytes that are read into the buffer.
93+
@_alwaysEmitIntoClient
94+
@discardableResult
95+
public func read(
96+
filling buffer: UnsafeMutableRawBufferPointer
97+
) throws -> Int {
98+
return try _read(filling: buffer).get()
99+
}
100+
101+
/// Reads bytes at the current file offset into a buffer until the buffer is filled.
102+
///
103+
/// - Parameters:
104+
/// - offset: The file offset where reading begins.
105+
/// - buffer: The region of memory to read into.
106+
/// - Returns: The number of bytes that were read, equal to `buffer.count`.
107+
///
108+
/// This method either reads until `buffer` is full, or throws an error if
109+
/// only part of the buffer was filled.
110+
///
111+
/// The <doc://com.apple.documentation/documentation/swift/unsafemutablerawbufferpointer/3019191-count> property of `buffer`
112+
/// determines the number of bytes that are read into the buffer.
113+
@_alwaysEmitIntoClient
114+
@discardableResult
115+
public func read(
116+
fromAbsoluteOffset offset: Int64,
117+
filling buffer: UnsafeMutableRawBufferPointer
118+
) throws -> Int {
119+
return try _read(fromAbsoluteOffset: offset, filling: buffer).get()
120+
}
121+
122+
@usableFromInline
123+
internal func _read(
124+
fromAbsoluteOffset offset: Int64? = nil,
125+
filling buffer: UnsafeMutableRawBufferPointer
126+
) -> Result<Int, Errno> {
127+
var idx = 0
128+
while idx < buffer.count {
129+
let readResult: Result<Int, Errno>
130+
if let offset = offset {
131+
readResult = _read(
132+
fromAbsoluteOffset: offset + Int64(idx),
133+
into: UnsafeMutableRawBufferPointer(rebasing: buffer[idx...]),
134+
retryOnInterrupt: true
135+
)
136+
} else {
137+
readResult = _readNoThrow(
138+
into: UnsafeMutableRawBufferPointer(rebasing: buffer[idx...]),
139+
retryOnInterrupt: true
140+
)
141+
}
142+
switch readResult {
143+
case .success(let numBytes): idx += numBytes
144+
case .failure(let err): return .failure(err)
145+
}
146+
}
147+
assert(idx == buffer.count)
148+
return .success(buffer.count)
149+
}
150+
82151
/// Writes a sequence of bytes to the given offset.
83152
///
84153
/// - Parameters:

Sources/System/FileOperations.swift

+10-1
Original file line numberDiff line numberDiff line change
@@ -158,14 +158,23 @@ extension FileDescriptor {
158158
into buffer: UnsafeMutableRawBufferPointer,
159159
retryOnInterrupt: Bool = true
160160
) throws -> Int {
161-
try _read(into: buffer, retryOnInterrupt: retryOnInterrupt).get()
161+
try _readNoThrow(into: buffer, retryOnInterrupt: retryOnInterrupt).get()
162162
}
163163

164+
// NOTE: This function (mistakenly marked as throws) is vestigial but remains to preserve ABI.
164165
@usableFromInline
165166
internal func _read(
166167
into buffer: UnsafeMutableRawBufferPointer,
167168
retryOnInterrupt: Bool
168169
) throws -> Result<Int, Errno> {
170+
_readNoThrow(into: buffer, retryOnInterrupt: retryOnInterrupt)
171+
}
172+
173+
@usableFromInline
174+
internal func _readNoThrow(
175+
into buffer: UnsafeMutableRawBufferPointer,
176+
retryOnInterrupt: Bool
177+
) -> Result<Int, Errno> {
169178
valueOrErrno(retryOnInterrupt: retryOnInterrupt) {
170179
system_read(self.rawValue, buffer.baseAddress, buffer.count)
171180
}

Tests/SystemTests/FileOperationsTest.swift

+32-1
Original file line numberDiff line numberDiff line change
@@ -160,5 +160,36 @@ final class FileOperationsTest: XCTestCase {
160160
issue26.runAllTests()
161161

162162
}
163-
}
164163

164+
/// This `#if` is present because, While `read(filling:)` is available on all platforms, this test
165+
/// makes use of `FileDescriptor.pipe()` which is not available on Windows.
166+
#if !os(Windows)
167+
func testReadFilling() async throws {
168+
let pipe = try FileDescriptor.pipe()
169+
defer {
170+
try? pipe.writeEnd.close()
171+
try? pipe.readEnd.close()
172+
}
173+
var abc = "abc"
174+
var def = "def"
175+
let abcdef = abc + def
176+
177+
try abc.withUTF8 {
178+
XCTAssertEqual(try pipe.writeEnd.writeAll(UnsafeRawBufferPointer($0)), 3)
179+
}
180+
181+
async let readBytes = try Array<UInt8>(unsafeUninitializedCapacity: abcdef.count) { buf, count in
182+
count = try pipe.readEnd.read(filling: UnsafeMutableRawBufferPointer(buf))
183+
XCTAssertEqual(count, abcdef.count)
184+
}
185+
186+
try def.withUTF8 {
187+
XCTAssertEqual(try pipe.writeEnd.writeAll(UnsafeRawBufferPointer($0)), 3)
188+
}
189+
190+
let _readBytes = try await readBytes
191+
192+
XCTAssertEqual(_readBytes, Array(abcdef.utf8))
193+
}
194+
#endif
195+
}

0 commit comments

Comments
 (0)