diff --git a/Sources/System/FileHelpers.swift b/Sources/System/FileHelpers.swift index af7cedfe..75536011 100644 --- a/Sources/System/FileHelpers.swift +++ b/Sources/System/FileHelpers.swift @@ -33,6 +33,78 @@ extension FileDescriptor { return result } + /// Reads bytes at the current file offset into a buffer until the buffer is filled or until EOF is reached. + /// + /// - Parameters: + /// - buffer: The region of memory to read into. + /// - Returns: The number of bytes that were read, at most `buffer.count`. + /// + /// This method either reads until `buffer` is full, or throws an error if + /// only part of the buffer was filled. + /// + /// The property of `buffer` + /// determines the number of bytes that are read into the buffer. + @_alwaysEmitIntoClient + @discardableResult + public func read( + filling buffer: UnsafeMutableRawBufferPointer + ) throws -> Int { + return try _read(filling: buffer).get() + } + + /// Reads bytes at the given file offset into a buffer until the buffer is filled or until EOF is reached. + /// + /// - Parameters: + /// - offset: The file offset where reading begins. + /// - buffer: The region of memory to read into. + /// - Returns: The number of bytes that were read, at most `buffer.count`. + /// + /// This method either reads until `buffer` is full, or throws an error if + /// only part of the buffer was filled. + /// + /// Unlike ``read(filling:)``, this method preserves the file descriptor's existing offset. + /// + /// The property of `buffer` + /// determines the number of bytes that are read into the buffer. + @_alwaysEmitIntoClient + @discardableResult + public func read( + fromAbsoluteOffset offset: Int64, + filling buffer: UnsafeMutableRawBufferPointer + ) throws -> Int { + return try _read(fromAbsoluteOffset: offset, filling: buffer).get() + } + + @usableFromInline + internal func _read( + fromAbsoluteOffset offset: Int64? = nil, + filling buffer: UnsafeMutableRawBufferPointer + ) -> Result { + var idx = 0 + loop: while idx < buffer.count { + let readResult: Result + if let offset = offset { + readResult = _read( + fromAbsoluteOffset: offset + Int64(idx), + into: UnsafeMutableRawBufferPointer(rebasing: buffer[idx...]), + retryOnInterrupt: true + ) + } else { + readResult = _readNoThrow( + into: UnsafeMutableRawBufferPointer(rebasing: buffer[idx...]), + retryOnInterrupt: true + ) + } + switch readResult { + case .success(let numBytes) where numBytes == 0: break loop // EOF + case .success(let numBytes): idx += numBytes + case .failure(let err): return .failure(err) + } + } + assert(idx <= buffer.count) + return .success(idx) + } + /// Writes a sequence of bytes to the current offset /// and then updates the offset. /// @@ -46,6 +118,9 @@ extension FileDescriptor { /// increments that position by the number of bytes written. /// See also ``seek(offset:from:)``. /// + /// This method either writes the entire contents of `sequence`, + /// or throws an error if only part of the content was written. + /// /// If `sequence` doesn't implement /// the method, /// temporary space will be allocated as needed. diff --git a/Sources/System/FileOperations.swift b/Sources/System/FileOperations.swift index 01025632..742bddc2 100644 --- a/Sources/System/FileOperations.swift +++ b/Sources/System/FileOperations.swift @@ -158,14 +158,23 @@ extension FileDescriptor { into buffer: UnsafeMutableRawBufferPointer, retryOnInterrupt: Bool = true ) throws -> Int { - try _read(into: buffer, retryOnInterrupt: retryOnInterrupt).get() + try _readNoThrow(into: buffer, retryOnInterrupt: retryOnInterrupt).get() } + // NOTE: This function (mistakenly marked as throws) is vestigial but remains to preserve ABI. @usableFromInline internal func _read( into buffer: UnsafeMutableRawBufferPointer, retryOnInterrupt: Bool ) throws -> Result { + _readNoThrow(into: buffer, retryOnInterrupt: retryOnInterrupt) + } + + @usableFromInline + internal func _readNoThrow( + into buffer: UnsafeMutableRawBufferPointer, + retryOnInterrupt: Bool + ) -> Result { valueOrErrno(retryOnInterrupt: retryOnInterrupt) { system_read(self.rawValue, buffer.baseAddress, buffer.count) } diff --git a/Tests/SystemTests/FileOperationsTest.swift b/Tests/SystemTests/FileOperationsTest.swift index e28efd5d..661a46a0 100644 --- a/Tests/SystemTests/FileOperationsTest.swift +++ b/Tests/SystemTests/FileOperationsTest.swift @@ -160,5 +160,64 @@ final class FileOperationsTest: XCTestCase { issue26.runAllTests() } -} + func testReadFillingFromFileEOF() async throws { + // Create a temporary file. + let tmpPath = "/tmp/\(UUID().uuidString)" + defer { + unlink(tmpPath) + } + + // Write some bytes. + var abc = "abc" + let byteCount = abc.count + let writeFd = try FileDescriptor.open(tmpPath, .readWrite, options: [.create, .truncate], permissions: .ownerReadWrite) + try writeFd.closeAfter { + try abc.withUTF8 { + XCTAssertEqual(try writeFd.writeAll(UnsafeRawBufferPointer($0)), byteCount) + } + } + + // Try and read more bytes than were written. + let readFd = try FileDescriptor.open(tmpPath, .readOnly) + try readFd.closeAfter { + let readBytes = try Array(unsafeUninitializedCapacity: byteCount + 1) { buf, count in + count = try readFd.read(filling: UnsafeMutableRawBufferPointer(buf)) + XCTAssertEqual(count, byteCount) + } + XCTAssertEqual(readBytes, Array(abc.utf8)) + } + } + +/// This `#if` is present because, While `read(filling:)` is available on all platforms, this test +/// makes use of `FileDescriptor.pipe()` which is not available on Windows. +#if !os(Windows) + func testReadFillingFromPipe() async throws { + let pipe = try FileDescriptor.pipe() + defer { + try? pipe.writeEnd.close() + try? pipe.readEnd.close() + } + var abc = "abc" + var def = "def" + let abcdef = abc + def + + try abc.withUTF8 { + XCTAssertEqual(try pipe.writeEnd.writeAll(UnsafeRawBufferPointer($0)), 3) + } + + async let readBytes = try Array(unsafeUninitializedCapacity: abcdef.count) { buf, count in + count = try pipe.readEnd.read(filling: UnsafeMutableRawBufferPointer(buf)) + XCTAssertEqual(count, abcdef.count) + } + + try def.withUTF8 { + XCTAssertEqual(try pipe.writeEnd.writeAll(UnsafeRawBufferPointer($0)), 3) + } + + let _readBytes = try await readBytes + + XCTAssertEqual(_readBytes, Array(abcdef.utf8)) + } +#endif +}