Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 160 additions & 22 deletions Sources/System/FileHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,18 +62,25 @@ extension FileDescriptor {
_ sequence: S
) -> Result<Int, Errno> where S.Element == UInt8 {
sequence._withRawBufferPointer { buffer in
var idx = 0
while idx < buffer.count {
switch _write(
UnsafeRawBufferPointer(rebasing: buffer[idx...]), retryOnInterrupt: true
) {
case .success(let numBytes): idx += numBytes
case .failure(let err): return .failure(err)
}
_writeAllBuffer(buffer)
}
}

@_alwaysEmitIntoClient
internal func _writeAllBuffer(
_ buffer: UnsafeRawBufferPointer
) -> Result<Int, Errno> {
var idx = 0
while idx < buffer.count {
switch _write(
UnsafeRawBufferPointer(rebasing: buffer[idx...]), retryOnInterrupt: true
) {
case .success(let numBytes): idx += numBytes
case .failure(let err): return .failure(err)
}
assert(idx == buffer.count)
return .success(buffer.count)
}
assert(idx == buffer.count)
return .success(buffer.count)
}

/// Writes a sequence of bytes to the given offset.
Expand Down Expand Up @@ -104,19 +111,150 @@ extension FileDescriptor {
toAbsoluteOffset offset: Int64, _ sequence: S
) -> Result<Int, Errno> where S.Element == UInt8 {
sequence._withRawBufferPointer { buffer in
var idx = 0
while idx < buffer.count {
switch _write(
toAbsoluteOffset: offset + Int64(idx),
UnsafeRawBufferPointer(rebasing: buffer[idx...]),
retryOnInterrupt: true
) {
case .success(let numBytes): idx += numBytes
case .failure(let err): return .failure(err)
}
_writeAllBuffer(toAbsoluteOffset: offset, buffer)
}
}

@_alwaysEmitIntoClient
internal func _writeAllBuffer(
toAbsoluteOffset offset: Int64, _ buffer: UnsafeRawBufferPointer
) -> Result<Int, Errno> {
var idx = 0
while idx < buffer.count {
switch _write(
toAbsoluteOffset: offset + Int64(idx),
UnsafeRawBufferPointer(rebasing: buffer[idx...]),
retryOnInterrupt: true
) {
case .success(let numBytes): idx += numBytes
case .failure(let err): return .failure(err)
}
}
assert(idx == buffer.count)
return .success(buffer.count)
}

/// Writes the entire contents of a buffer, retrying on partial writes.
///
/// - Parameters:
/// - data: The region of memory that contains the data being written.
/// - retryOnInterrupt: Whether to retry the write operation
/// if it throws ``Errno/interrupted``.
/// The default is `true`.
/// Pass `false` to try only once and throw an error upon interruption.
/// - Returns: The number of bytes that were written.
///
/// After writing,
/// this method increments the file's offset by the number of bytes written.
/// To change the file's offset,
/// call the ``seek(offset:from:)`` method.
///
/// The corresponding C function is `write`.
@available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@glessard could you help me adopt/add the right availability macro or versioning for this?

@_alwaysEmitIntoClient
public func writeAll(
_ data: RawSpan,
retryOnInterrupt: Bool = true
) throws(Errno) -> Int {
try data.withUnsafeBytes { buffer throws(Errno) -> Int in
try _writeAllBuffer(buffer).get()
}
}

/// Writes the entire contents of a buffer at the specified offset,
/// retrying on partial writes.
///
/// - Parameters:
/// - offset: The file offset where writing begins.
/// - data: The region of memory that contains the data being written.
/// - retryOnInterrupt: Whether to retry the write operation
/// if it throws ``Errno/interrupted``.
/// The default is `true`.
/// Pass `false` to try only once and throw an error upon interruption.
/// - Returns: The number of bytes that were written.
///
/// Unlike ``writeAll(_:retryOnInterrupt:)``,
/// this method leaves the file's existing offset unchanged.
///
/// The corresponding C function is `pwrite`.
@available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *)
@_alwaysEmitIntoClient
public func writeAll(
toAbsoluteOffset offset: Int64,
_ data: RawSpan,
retryOnInterrupt: Bool = true
) throws(Errno) -> Int {
try data.withUnsafeBytes { buffer throws(Errno) -> Int in
try _writeAllBuffer(toAbsoluteOffset: offset, buffer).get()
}
}

/// Reads bytes into a buffer, retrying until it is full.
///
/// - Parameters:
/// - buffer: The region of memory to read into.
/// - retryOnInterrupt: Whether to retry the read operation
/// if it throws ``Errno/interrupted``.
/// The default is `true`.
/// Pass `false` to try only once and throw an error upon interruption.
/// - Returns: The number of bytes that were read.
///
/// After reading,
/// this method increments the file's offset by the number of bytes read.
/// To change the file's offset,
/// call the ``seek(offset:from:)`` method.
///
/// The corresponding C function is `read`.
@available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *)
@_alwaysEmitIntoClient
@discardableResult
public func read(
filling buffer: inout OutputRawSpan,
retryOnInterrupt: Bool = true
) throws(Errno) -> Int {
let originalCapacity = buffer.freeCapacity
while buffer.freeCapacity > 0 {
let bytesRead = try read(into: &buffer, retryOnInterrupt: retryOnInterrupt)
if bytesRead == 0 {
break // EOF
}
}
return originalCapacity - buffer.freeCapacity
}

/// Reads bytes at the specified offset into a buffer,
/// retrying until it is full.
///
/// - Parameters:
/// - offset: The file offset where reading begins.
/// - buffer: The region of memory to read into.
/// - retryOnInterrupt: Whether to retry the read operation
/// if it throws ``Errno/interrupted``.
/// The default is `true`.
/// Pass `false` to try only once and throw an error upon interruption.
/// - Returns: The number of bytes that were read.
///
/// Unlike ``read(filling:retryOnInterrupt:)``,
/// this method leaves the file's existing offset unchanged.
///
/// The corresponding C function is `pread`.
@available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *)
@_alwaysEmitIntoClient
@discardableResult
public func read(
fromAbsoluteOffset offset: Int64,
filling buffer: inout OutputRawSpan,
retryOnInterrupt: Bool = true
) throws(Errno) -> Int {
let originalCapacity = buffer.freeCapacity
var currentOffset = offset
while buffer.freeCapacity > 0 {
let bytesRead = try read(fromAbsoluteOffset: currentOffset, into: &buffer, retryOnInterrupt: retryOnInterrupt)
if bytesRead == 0 {
break // EOF
}
assert(idx == buffer.count)
return .success(buffer.count)
currentOffset += Int64(bytesRead)
}
return originalCapacity - buffer.freeCapacity
}
}
144 changes: 144 additions & 0 deletions Sources/System/FileOperations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,150 @@ extension FileDescriptor {
buffer,
retryOnInterrupt: retryOnInterrupt)
}

/// Writes the contents of a buffer at the current file offset.
///
/// - Parameters:
/// - data: The region of memory that contains the data being written.
/// - retryOnInterrupt: Whether to retry the write operation
/// if it throws ``Errno/interrupted``.
/// The default is `true`.
/// Pass `false` to try only once and throw an error upon interruption.
/// - Returns: The number of bytes that were written.
///
/// After writing,
/// this method increments the file's offset by the number of bytes written.
/// To change the file's offset,
/// call the ``seek(offset:from:)`` method.
///
/// The corresponding C function is `write`.
@available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *)
@_alwaysEmitIntoClient
public func write(
_ data: RawSpan,
retryOnInterrupt: Bool = true
) throws(Errno) -> Int {
try data.withUnsafeBytes { bytes throws(Errno) -> Int in
try _write(bytes, retryOnInterrupt: retryOnInterrupt).get()
}
}

/// Writes the contents of a buffer at the specified offset.
///
/// - Parameters:
/// - offset: The file offset where writing begins.
/// - data: The region of memory that contains the data being written.
/// - retryOnInterrupt: Whether to retry the write operation
/// if it throws ``Errno/interrupted``.
/// The default is `true`.
/// Pass `false` to try only once and throw an error upon interruption.
/// - Returns: The number of bytes that were written.
///
/// Unlike ``write(_:retryOnInterrupt:)``,
/// this method leaves the file's existing offset unchanged.
///
/// The corresponding C function is `pwrite`.
@available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *)
@_alwaysEmitIntoClient
public func write(
toAbsoluteOffset offset: Int64,
_ data: RawSpan,
retryOnInterrupt: Bool = true
) throws(Errno) -> Int {
try data.withUnsafeBytes { bytes throws(Errno) -> Int in
try _write(toAbsoluteOffset: offset, bytes, retryOnInterrupt: retryOnInterrupt).get()
}
}

@available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *)
@_alwaysEmitIntoClient
internal func _readIntoOutputBuffer(
_ buffer: UnsafeMutableRawBufferPointer,
initializedCount: inout Int,
retryOnInterrupt: Bool
) -> Result<Int, Errno> {
// Read into the uninitialized portion (starting at offset 'initializedCount')
let uninitializedPortion = UnsafeMutableRawBufferPointer(
start: buffer.baseAddress?.advanced(by: initializedCount),
count: buffer.count - initializedCount
)
do {
let result = try _read(into: uninitializedPortion, retryOnInterrupt: retryOnInterrupt)
if case .success(let bytesRead) = result {
initializedCount += bytesRead // Add to existing count, don't replace it!
}
return result
} catch {
fatalError("Unexpected error from _read")
}
}

/// Reads bytes at the current file offset into a buffer.
///
/// - Parameters:
/// - buffer: The region of memory to read into.
/// - retryOnInterrupt: Whether to retry the read operation
/// if it throws ``Errno/interrupted``.
/// The default is `true`.
/// Pass `false` to try only once and throw an error upon interruption.
/// - Returns: The number of bytes that were read.
///
/// After reading,
/// this method increments the file's offset by the number of bytes read.
/// To change the file's offset,
/// call the ``seek(offset:from:)`` method.
///
/// The corresponding C function is `read`.
@available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *)
@_alwaysEmitIntoClient
public func read(
into buffer: inout OutputRawSpan,
retryOnInterrupt: Bool = true
) throws(Errno) -> Int {
try buffer.withUnsafeMutableBytes { buf, count throws(Errno) -> Int in
try _readIntoOutputBuffer(buf, initializedCount: &count, retryOnInterrupt: retryOnInterrupt).get()
}
}

/// Reads bytes at the specified offset into a buffer.
///
/// - Parameters:
/// - offset: The file offset where reading begins.
/// - buffer: The region of memory to read into.
/// - retryOnInterrupt: Whether to retry the read operation
/// if it throws ``Errno/interrupted``.
/// The default is `true`.
/// Pass `false` to try only once and throw an error upon interruption.
/// - Returns: The number of bytes that were read.
///
/// Unlike ``read(into:retryOnInterrupt:)``,
/// this method leaves the file's existing offset unchanged.
///
/// The corresponding C function is `pread`.
@available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *)
@_alwaysEmitIntoClient
public func read(
fromAbsoluteOffset offset: Int64,
into buffer: inout OutputRawSpan,
retryOnInterrupt: Bool = true
) throws(Errno) -> Int {
do {
return try buffer.withUnsafeMutableBytes { buf, count in
// Read into the uninitialized portion (starting at offset 'count')
let uninitializedPortion = UnsafeMutableRawBufferPointer(
start: buf.baseAddress?.advanced(by: count),
count: buf.count - count
)
let bytesRead = try read(fromAbsoluteOffset: offset, into: uninitializedPortion, retryOnInterrupt: retryOnInterrupt)
count += bytesRead // Add to existing count, don't replace it!
return bytesRead
}
} catch let error as Errno {
throw error
} catch {
fatalError("Unexpected error type")
}
}
}

#if !os(WASI)
Expand Down
Loading
Loading