Coverage Report

Created: 2026-04-29 06:25

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/swift-nio/Sources/NIOPosix/NonBlockingFileIO.swift
Line
Count
Source
1
//===----------------------------------------------------------------------===//
2
//
3
// This source file is part of the SwiftNIO open source project
4
//
5
// Copyright (c) 2017-2024 Apple Inc. and the SwiftNIO project authors
6
// Licensed under Apache License v2.0
7
//
8
// See LICENSE.txt for license information
9
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10
//
11
// SPDX-License-Identifier: Apache-2.0
12
//
13
//===----------------------------------------------------------------------===//
14
15
#if !os(WASI)
16
17
import CNIOLinux
18
import CNIOOpenBSD
19
import CNIOWindows
20
import NIOConcurrencyHelpers
21
import NIOCore
22
23
/// ``NonBlockingFileIO`` is a helper that allows you to read files without blocking the calling thread.
24
///
25
/// - warning: The `NonBlockingFileIO` API is deprecated, do not use going forward. It's not marked as `deprecated` yet such
26
///            that users don't get the deprecation warnings affecting their APIs everywhere. For file I/O, please use
27
///            the `NIOFileSystem` API.
28
///
29
/// It is worth noting that `kqueue`, `epoll` or `poll` returning claiming a file is readable does not mean that the
30
/// data is already available in the kernel's memory. In other words, a `read` from a file can still block even if
31
/// reported as readable. This behaviour is also documented behaviour:
32
///
33
///  - [`poll`](http://pubs.opengroup.org/onlinepubs/009695399/functions/poll.html): "Regular files shall always poll TRUE for reading and writing."
34
///  - [`epoll`](http://man7.org/linux/man-pages/man7/epoll.7.html): "epoll is simply a faster poll(2), and can be used wherever the latter is used since it shares the same semantics."
35
///  - [`kqueue`](https://www.freebsd.org/cgi/man.cgi?query=kqueue&sektion=2): "Returns when the file pointer is not at the end of file."
36
///
37
/// ``NonBlockingFileIO`` helps to work around this issue by maintaining its own thread pool that is used to read the data
38
/// from the files into memory. It will then hand the (in-memory) data back which makes it available without the possibility
39
/// of blocking.
40
public struct NonBlockingFileIO: Sendable {
41
    /// The default and recommended size for ``NonBlockingFileIO``'s thread pool.
42
    @inlinable
43
0
    public static var defaultThreadPoolSize: Int { 2 }
44
45
    /// The default and recommended chunk size.
46
    @inlinable
47
0
    public static var defaultChunkSize: Int { 128 * 1024 }
48
49
    /// ``NonBlockingFileIO`` errors.
50
    public enum Error: Swift.Error {
51
        /// ``NonBlockingFileIO`` is meant to be used with file descriptors that are set to the default (blocking) mode.
52
        /// It doesn't make sense to use it with a file descriptor where `O_NONBLOCK` is set therefore this error is
53
        /// raised when that was requested.
54
        case descriptorSetToNonBlocking
55
    }
56
57
    private let threadPool: NIOThreadPool
58
59
    /// Initialize a ``NonBlockingFileIO`` which uses the `NIOThreadPool`.
60
    ///
61
    /// - Parameters:
62
    ///   - threadPool: The `NIOThreadPool` that will be used for all the IO.
63
0
    public init(threadPool: NIOThreadPool) {
64
0
        self.threadPool = threadPool
65
0
    }
66
67
    /// Read a `FileRegion` in chunks of `chunkSize` bytes on ``NonBlockingFileIO``'s private thread
68
    /// pool which is separate from any `EventLoop` thread.
69
    ///
70
    /// `chunkHandler` will be called on `eventLoop` for every chunk that was read. Assuming `fileRegion.readableBytes` is greater than
71
    /// zero and there are enough bytes available `chunkHandler` will be called `1 + |_ fileRegion.readableBytes / chunkSize _|`
72
    /// times, delivering `chunkSize` bytes each time. If less than `fileRegion.readableBytes` bytes can be read from the file,
73
    /// `chunkHandler` will be called less often with the last invocation possibly being of less than `chunkSize` bytes.
74
    ///
75
    /// The allocation and reading of a subsequent chunk will only be attempted when `chunkHandler` succeeds.
76
    ///
77
    /// This method will not use the file descriptor's seek pointer which means there is no danger of reading from the
78
    /// same `FileRegion` in multiple threads.
79
    ///
80
    /// - Parameters:
81
    ///   - fileRegion: The file region to read.
82
    ///   - chunkSize: The size of the individual chunks to deliver.
83
    ///   - allocator: A `ByteBufferAllocator` used to allocate space for the chunks.
84
    ///   - eventLoop: The `EventLoop` to call `chunkHandler` on.
85
    ///   - chunkHandler: Called for every chunk read. The next chunk will be read upon successful completion of the returned `EventLoopFuture`. If the returned `EventLoopFuture` fails, the overall operation is aborted.
86
    /// - Returns: An `EventLoopFuture` which is the result of the overall operation. If either the reading of `fileHandle` or `chunkHandler` fails, the `EventLoopFuture` will fail too. If the reading of `fileHandle` as well as `chunkHandler` always succeeded, the `EventLoopFuture` will succeed too.
87
    @preconcurrency
88
    public func readChunked(
89
        fileRegion: FileRegion,
90
        chunkSize: Int = NonBlockingFileIO.defaultChunkSize,
91
        allocator: ByteBufferAllocator,
92
        eventLoop: EventLoop,
93
        chunkHandler: @escaping @Sendable (ByteBuffer) -> EventLoopFuture<Void>
94
0
    ) -> EventLoopFuture<Void> {
95
0
        let readableBytes = fileRegion.readableBytes
96
0
        return self.readChunked(
97
0
            fileHandle: fileRegion.fileHandle,
98
0
            fromOffset: Int64(fileRegion.readerIndex),
99
0
            byteCount: readableBytes,
100
0
            chunkSize: chunkSize,
101
0
            allocator: allocator,
102
0
            eventLoop: eventLoop,
103
0
            chunkHandler: chunkHandler
104
0
        )
105
0
    }
106
107
    /// Read `byteCount` bytes in chunks of `chunkSize` bytes from `fileHandle` in ``NonBlockingFileIO``'s private thread
108
    /// pool which is separate from any `EventLoop` thread.
109
    ///
110
    /// `chunkHandler` will be called on `eventLoop` for every chunk that was read. Assuming `byteCount` is greater than
111
    /// zero and there are enough bytes available `chunkHandler` will be called `1 + |_ byteCount / chunkSize _|`
112
    /// times, delivering `chunkSize` bytes each time. If less than `byteCount` bytes can be read from `descriptor`,
113
    /// `chunkHandler` will be called less often with the last invocation possibly being of less than `chunkSize` bytes.
114
    ///
115
    /// The allocation and reading of a subsequent chunk will only be attempted when `chunkHandler` succeeds.
116
    ///
117
    /// - Note: `readChunked(fileRegion:chunkSize:allocator:eventLoop:chunkHandler:)` should be preferred as it uses
118
    ///         `FileRegion` object instead of raw `NIOFileHandle`s. In case you do want to use raw `NIOFileHandle`s,
119
    ///         please consider using `readChunked(fileHandle:fromOffset:chunkSize:allocator:eventLoop:chunkHandler:)`
120
    ///         because it doesn't use the file descriptor's seek pointer (which may be shared with other file
121
    ///         descriptors and even across processes.)
122
    ///
123
    /// - Parameters:
124
    ///   - fileHandle: The `NIOFileHandle` to read from.
125
    ///   - byteCount: The number of bytes to read from `fileHandle`.
126
    ///   - chunkSize: The size of the individual chunks to deliver.
127
    ///   - allocator: A `ByteBufferAllocator` used to allocate space for the chunks.
128
    ///   - eventLoop: The `EventLoop` to call `chunkHandler` on.
129
    ///   - chunkHandler: Called for every chunk read. The next chunk will be read upon successful completion of the returned `EventLoopFuture`. If the returned `EventLoopFuture` fails, the overall operation is aborted.
130
    /// - Returns: An `EventLoopFuture` which is the result of the overall operation. If either the reading of `fileHandle` or `chunkHandler` fails, the `EventLoopFuture` will fail too. If the reading of `fileHandle` as well as `chunkHandler` always succeeded, the `EventLoopFuture` will succeed too.
131
    @preconcurrency
132
    public func readChunked(
133
        fileHandle: NIOFileHandle,
134
        byteCount: Int,
135
        chunkSize: Int = NonBlockingFileIO.defaultChunkSize,
136
        allocator: ByteBufferAllocator,
137
        eventLoop: EventLoop,
138
        chunkHandler: @escaping @Sendable (ByteBuffer) -> EventLoopFuture<Void>
139
0
    ) -> EventLoopFuture<Void> {
140
0
        self.readChunked0(
141
0
            fileHandle: fileHandle,
142
0
            fromOffset: nil,
143
0
            byteCount: byteCount,
144
0
            chunkSize: chunkSize,
145
0
            allocator: allocator,
146
0
            eventLoop: eventLoop,
147
0
            chunkHandler: chunkHandler
148
0
        )
149
0
    }
150
151
    /// Read `byteCount` bytes from offset `fileOffset` in chunks of `chunkSize` bytes from `fileHandle` in ``NonBlockingFileIO``'s private thread
152
    /// pool which is separate from any `EventLoop` thread.
153
    ///
154
    /// `chunkHandler` will be called on `eventLoop` for every chunk that was read. Assuming `byteCount` is greater than
155
    /// zero and there are enough bytes available `chunkHandler` will be called `1 + |_ byteCount / chunkSize _|`
156
    /// times, delivering `chunkSize` bytes each time. If less than `byteCount` bytes can be read from `descriptor`,
157
    /// `chunkHandler` will be called less often with the last invocation possibly being of less than `chunkSize` bytes.
158
    ///
159
    /// The allocation and reading of a subsequent chunk will only be attempted when `chunkHandler` succeeds.
160
    ///
161
    /// This method will not use the file descriptor's seek pointer which means there is no danger of reading from the
162
    /// same `NIOFileHandle` in multiple threads.
163
    ///
164
    /// - Note: `readChunked(fileRegion:chunkSize:allocator:eventLoop:chunkHandler:)` should be preferred as it uses
165
    ///         `FileRegion` object instead of raw `NIOFileHandle`s.
166
    ///
167
    /// - Parameters:
168
    ///   - fileHandle: The `NIOFileHandle` to read from.
169
    ///   - fileOffset: The offset into the file at which the read should begin.
170
    ///   - byteCount: The number of bytes to read from `fileHandle`.
171
    ///   - chunkSize: The size of the individual chunks to deliver.
172
    ///   - allocator: A `ByteBufferAllocator` used to allocate space for the chunks.
173
    ///   - eventLoop: The `EventLoop` to call `chunkHandler` on.
174
    ///   - chunkHandler: Called for every chunk read. The next chunk will be read upon successful completion of the returned `EventLoopFuture`. If the returned `EventLoopFuture` fails, the overall operation is aborted.
175
    /// - Returns: An `EventLoopFuture` which is the result of the overall operation. If either the reading of `fileHandle` or `chunkHandler` fails, the `EventLoopFuture` will fail too. If the reading of `fileHandle` as well as `chunkHandler` always succeeded, the `EventLoopFuture` will succeed too.
176
    @preconcurrency
177
    public func readChunked(
178
        fileHandle: NIOFileHandle,
179
        fromOffset fileOffset: Int64,
180
        byteCount: Int,
181
        chunkSize: Int = NonBlockingFileIO.defaultChunkSize,
182
        allocator: ByteBufferAllocator,
183
        eventLoop: EventLoop,
184
        chunkHandler: @escaping @Sendable (ByteBuffer) -> EventLoopFuture<Void>
185
0
    ) -> EventLoopFuture<Void> {
186
0
        self.readChunked0(
187
0
            fileHandle: fileHandle,
188
0
            fromOffset: fileOffset,
189
0
            byteCount: byteCount,
190
0
            chunkSize: chunkSize,
191
0
            allocator: allocator,
192
0
            eventLoop: eventLoop,
193
0
            chunkHandler: chunkHandler
194
0
        )
195
0
    }
196
197
    private typealias ReadChunkHandler = @Sendable (ByteBuffer) -> EventLoopFuture<Void>
198
199
    private func readChunked0(
200
        fileHandle: NIOFileHandle,
201
        fromOffset: Int64?,
202
        byteCount: Int,
203
        chunkSize: Int,
204
        allocator: ByteBufferAllocator,
205
        eventLoop: EventLoop,
206
        chunkHandler: @escaping ReadChunkHandler
207
0
    ) -> EventLoopFuture<Void> {
208
0
        precondition(chunkSize > 0, "chunkSize must be > 0 (is \(chunkSize))")
209
0
        let remainingReads = 1 + (byteCount / chunkSize)
210
0
        let lastReadSize = byteCount % chunkSize
211
0
212
0
        let promise = eventLoop.makePromise(of: Void.self)
213
0
214
0
        @Sendable
215
0
        func _read(remainingReads: Int, bytesReadSoFar: Int64) {
216
0
            if remainingReads > 1 || (remainingReads == 1 && lastReadSize > 0) {
217
0
                let readSize = remainingReads > 1 ? chunkSize : lastReadSize
218
0
                assert(readSize > 0)
219
0
                let readFuture = self.read0(
220
0
                    fileHandle: fileHandle,
221
0
                    fromOffset: fromOffset.map { $0 + bytesReadSoFar },
222
0
                    byteCount: readSize,
223
0
                    allocator: allocator,
224
0
                    eventLoop: eventLoop
225
0
                )
226
0
                readFuture.whenComplete { (result) in
227
0
                    switch result {
228
0
                    case .success(let buffer):
229
0
                        guard buffer.readableBytes > 0 else {
230
0
                            // EOF, call `chunkHandler` one more time.
231
0
                            let handlerFuture = chunkHandler(buffer)
232
0
                            handlerFuture.cascade(to: promise)
233
0
                            return
234
0
                        }
235
0
                        let bytesRead = Int64(buffer.readableBytes)
236
0
                        chunkHandler(buffer).hop(to: eventLoop).whenComplete { result in
237
0
                            switch result {
238
0
                            case .success(_):
239
0
                                eventLoop.assertInEventLoop()
240
0
                                _read(
241
0
                                    remainingReads: remainingReads - 1,
242
0
                                    bytesReadSoFar: bytesReadSoFar + bytesRead
243
0
                                )
244
0
                            case .failure(let error):
245
0
                                promise.fail(error)
246
0
                            }
247
0
                        }
248
0
                    case .failure(let error):
249
0
                        promise.fail(error)
250
0
                    }
251
0
                }
252
0
            } else {
253
0
                promise.succeed(())
254
0
            }
255
0
        }
256
0
        _read(remainingReads: remainingReads, bytesReadSoFar: 0)
257
0
258
0
        return promise.futureResult
259
0
    }
260
261
    /// Read a `FileRegion` in ``NonBlockingFileIO``'s private thread pool which is separate from any `EventLoop` thread.
262
    ///
263
    /// The returned `ByteBuffer` will not have less than `fileRegion.readableBytes` unless we hit end-of-file in which
264
    /// case the `ByteBuffer` will contain the bytes available to read.
265
    ///
266
    /// This method will not use the file descriptor's seek pointer which means there is no danger of reading from the
267
    /// same `FileRegion` in multiple threads.
268
    ///
269
    /// - Note: Only use this function for small enough `FileRegion`s as it will need to allocate enough memory to hold `fileRegion.readableBytes` bytes.
270
    /// - Note: In most cases you should prefer one of the `readChunked` functions.
271
    ///
272
    /// - Parameters:
273
    ///   - fileRegion: The file region to read.
274
    ///   - allocator: A `ByteBufferAllocator` used to allocate space for the returned `ByteBuffer`.
275
    ///   - eventLoop: The `EventLoop` to create the returned `EventLoopFuture` from.
276
    /// - Returns: An `EventLoopFuture` which delivers a `ByteBuffer` if the read was successful or a failure on error.
277
    public func read(
278
        fileRegion: FileRegion,
279
        allocator: ByteBufferAllocator,
280
        eventLoop: EventLoop
281
0
    ) -> EventLoopFuture<ByteBuffer> {
282
0
        let readableBytes = fileRegion.readableBytes
283
0
        return self.read(
284
0
            fileHandle: fileRegion.fileHandle,
285
0
            fromOffset: Int64(fileRegion.readerIndex),
286
0
            byteCount: readableBytes,
287
0
            allocator: allocator,
288
0
            eventLoop: eventLoop
289
0
        )
290
0
    }
291
292
    /// Read `byteCount` bytes from `fileHandle` in ``NonBlockingFileIO``'s private thread pool which is separate from any `EventLoop` thread.
293
    ///
294
    /// The returned `ByteBuffer` will not have less than `byteCount` bytes unless we hit end-of-file in which
295
    /// case the `ByteBuffer` will contain the bytes available to read.
296
    ///
297
    /// - Note: Only use this function for small enough `byteCount`s as it will need to allocate enough memory to hold `byteCount` bytes.
298
    /// - Note: ``read(fileRegion:allocator:eventLoop:)`` should be preferred as it uses `FileRegion` object instead of
299
    ///         raw `NIOFileHandle`s. In case you do want to use raw `NIOFileHandle`s,
300
    ///         please consider using ``read(fileHandle:fromOffset:byteCount:allocator:eventLoop:)``
301
    ///         because it doesn't use the file descriptor's seek pointer (which may be shared with other file
302
    ///         descriptors and even across processes.)
303
    ///
304
    /// - Parameters:
305
    ///   - fileHandle: The `NIOFileHandle` to read.
306
    ///   - byteCount: The number of bytes to read from `fileHandle`.
307
    ///   - allocator: A `ByteBufferAllocator` used to allocate space for the returned `ByteBuffer`.
308
    ///   - eventLoop: The `EventLoop` to create the returned `EventLoopFuture` from.
309
    /// - Returns: An `EventLoopFuture` which delivers a `ByteBuffer` if the read was successful or a failure on error.
310
    public func read(
311
        fileHandle: NIOFileHandle,
312
        byteCount: Int,
313
        allocator: ByteBufferAllocator,
314
        eventLoop: EventLoop
315
0
    ) -> EventLoopFuture<ByteBuffer> {
316
0
        self.read0(
317
0
            fileHandle: fileHandle,
318
0
            fromOffset: nil,
319
0
            byteCount: byteCount,
320
0
            allocator: allocator,
321
0
            eventLoop: eventLoop
322
0
        )
323
0
    }
324
325
    /// Read `byteCount` bytes starting at `fileOffset` from `fileHandle` in ``NonBlockingFileIO``'s private thread pool
326
    /// which is separate from any `EventLoop` thread.
327
    ///
328
    /// The returned `ByteBuffer` will not have less than `byteCount` bytes unless we hit end-of-file in which
329
    /// case the `ByteBuffer` will contain the bytes available to read.
330
    ///
331
    /// This method will not use the file descriptor's seek pointer which means there is no danger of reading from the
332
    /// same `fileHandle` in multiple threads.
333
    ///
334
    /// - Note: Only use this function for small enough `byteCount`s as it will need to allocate enough memory to hold `byteCount` bytes.
335
    /// - Note: ``read(fileRegion:allocator:eventLoop:)`` should be preferred as it uses `FileRegion` object instead of raw `NIOFileHandle`s.
336
    ///
337
    /// - Parameters:
338
    ///   - fileHandle: The `NIOFileHandle` to read.
339
    ///   - fileOffset: The offset to read from.
340
    ///   - byteCount: The number of bytes to read from `fileHandle`.
341
    ///   - allocator: A `ByteBufferAllocator` used to allocate space for the returned `ByteBuffer`.
342
    ///   - eventLoop: The `EventLoop` to create the returned `EventLoopFuture` from.
343
    /// - Returns: An `EventLoopFuture` which delivers a `ByteBuffer` if the read was successful or a failure on error.
344
    public func read(
345
        fileHandle: NIOFileHandle,
346
        fromOffset fileOffset: Int64,
347
        byteCount: Int,
348
        allocator: ByteBufferAllocator,
349
        eventLoop: EventLoop
350
0
    ) -> EventLoopFuture<ByteBuffer> {
351
0
        self.read0(
352
0
            fileHandle: fileHandle,
353
0
            fromOffset: fileOffset,
354
0
            byteCount: byteCount,
355
0
            allocator: allocator,
356
0
            eventLoop: eventLoop
357
0
        )
358
0
    }
359
360
    private func read0(
361
        fileHandle: NIOFileHandle,
362
        fromOffset: Int64?,  // > 2 GB offset is reasonable on 32-bit systems
363
        byteCount rawByteCount: Int,
364
        allocator: ByteBufferAllocator,
365
        eventLoop: EventLoop
366
0
    ) -> EventLoopFuture<ByteBuffer> {
367
0
        guard rawByteCount > 0 else {
368
0
            return eventLoop.makeSucceededFuture(allocator.buffer(capacity: 0))
369
0
        }
370
0
        let byteCount = rawByteCount < Int32.max ? rawByteCount : size_t(Int32.max)
371
0
372
0
        return self.threadPool.runIfActive(eventLoop: eventLoop) { () -> ByteBuffer in
373
0
            try self.readSync(
374
0
                fileHandle: fileHandle,
375
0
                fromOffset: fromOffset,
376
0
                byteCount: byteCount,
377
0
                allocator: allocator
378
0
            )
379
0
        }
380
0
    }
381
382
    private func readSync(
383
        fileHandle: NIOFileHandle,
384
        fromOffset: Int64?,  // > 2 GB offset is reasonable on 32-bit systems
385
        byteCount: Int,
386
        allocator: ByteBufferAllocator
387
0
    ) throws -> ByteBuffer {
388
0
        var bytesRead = 0
389
0
        var buf = allocator.buffer(capacity: byteCount)
390
0
391
0
        while bytesRead < byteCount {
392
0
            let n = try buf.writeWithUnsafeMutableBytes(minimumWritableBytes: byteCount - bytesRead) { ptr -> Int in
393
0
                let res = try fileHandle.withUnsafeFileDescriptor { descriptor -> IOResult<ssize_t> in
394
0
                    if let offset = fromOffset {
395
                        #if !os(Windows)
396
0
                        return try Posix.pread(
397
0
                            descriptor: descriptor,
398
0
                            pointer: ptr.baseAddress!,
399
0
                            size: byteCount - bytesRead,
400
0
                            offset: off_t(offset) + off_t(bytesRead)
401
0
                        )
402
                        #else
403
                        return try Windows.pread(
404
                            descriptor: descriptor,
405
                            pointer: ptr.baseAddress!,
406
                            size: byteCount - bytesRead,
407
                            offset: off_t(offset) + off_t(bytesRead)
408
                        )
409
                        #endif
410
0
                    }
411
0
412
0
                    return try Posix.read(
413
0
                        descriptor: descriptor,
414
0
                        pointer: ptr.baseAddress!,
415
0
                        size: byteCount - bytesRead
416
0
                    )
417
0
                }
418
0
                switch res {
419
0
                case .processed(let n):
420
0
                    assert(n >= 0, "read claims to have read a negative number of bytes \(n)")
421
0
                    return numericCast(n)  // ssize_t is Int64 on Windows and Int everywhere else.
422
0
                case .wouldBlock:
423
0
                    throw Error.descriptorSetToNonBlocking
424
0
                }
425
0
            }
426
0
            if n == 0 {
427
0
                // EOF
428
0
                break
429
0
            } else {
430
0
                bytesRead += n
431
0
            }
432
0
        }
433
0
        return buf
434
0
    }
435
436
    /// Changes the file size of `fileHandle` to `size`.
437
    ///
438
    /// If `size` is smaller than the current file size, the remaining bytes will be truncated and are lost. If `size`
439
    /// is larger than the current file size, the gap will be filled with zero bytes.
440
    ///
441
    /// - Parameters:
442
    ///   - fileHandle: The `NIOFileHandle` to write to.
443
    ///   - size: The new file size in bytes to write.
444
    ///   - eventLoop: The `EventLoop` to create the returned `EventLoopFuture` from.
445
    /// - Returns: An `EventLoopFuture` which is fulfilled if the write was successful or fails on error.
446
    public func changeFileSize(
447
        fileHandle: NIOFileHandle,
448
        size: Int64,
449
        eventLoop: EventLoop
450
0
    ) -> EventLoopFuture<()> {
451
0
        self.threadPool.runIfActive(eventLoop: eventLoop) {
452
0
            try fileHandle.withUnsafeFileDescriptor { descriptor -> Void in
453
0
                try Posix.ftruncate(descriptor: descriptor, size: off_t(size))
454
0
            }
455
0
        }
456
0
    }
457
458
    /// Returns the length of the file in bytes associated with `fileHandle`.
459
    ///
460
    /// - Parameters:
461
    ///   - fileHandle: The `NIOFileHandle` to read from.
462
    ///   - eventLoop: The `EventLoop` to create the returned `EventLoopFuture` from.
463
    /// - Returns: An `EventLoopFuture` which is fulfilled with the length of the file in bytes if the write was successful or fails on error.
464
    public func readFileSize(
465
        fileHandle: NIOFileHandle,
466
        eventLoop: EventLoop
467
0
    ) -> EventLoopFuture<Int64> {
468
0
        self.threadPool.runIfActive(eventLoop: eventLoop) {
469
0
            try fileHandle.withUnsafeFileDescriptor { descriptor in
470
0
                let curr = try Posix.lseek(descriptor: descriptor, offset: 0, whence: SEEK_CUR)
471
0
                let eof = try Posix.lseek(descriptor: descriptor, offset: 0, whence: SEEK_END)
472
0
                try Posix.lseek(descriptor: descriptor, offset: curr, whence: SEEK_SET)
473
0
                return Int64(eof)
474
0
            }
475
0
        }
476
0
    }
477
478
    /// Write `buffer` to `fileHandle` in ``NonBlockingFileIO``'s private thread pool which is separate from any `EventLoop` thread.
479
    ///
480
    /// - Parameters:
481
    ///   - fileHandle: The `NIOFileHandle` to write to.
482
    ///   - buffer: The `ByteBuffer` to write.
483
    ///   - eventLoop: The `EventLoop` to create the returned `EventLoopFuture` from.
484
    /// - Returns: An `EventLoopFuture` which is fulfilled if the write was successful or fails on error.
485
    public func write(
486
        fileHandle: NIOFileHandle,
487
        buffer: ByteBuffer,
488
        eventLoop: EventLoop
489
0
    ) -> EventLoopFuture<()> {
490
0
        self.write0(fileHandle: fileHandle, toOffset: nil, buffer: buffer, eventLoop: eventLoop)
491
0
    }
492
493
    /// Write `buffer` starting from `toOffset` to `fileHandle` in ``NonBlockingFileIO``'s private thread pool which is separate from any `EventLoop` thread.
494
    ///
495
    /// - Parameters:
496
    ///   - fileHandle: The `NIOFileHandle` to write to.
497
    ///   - toOffset: The file offset to write to.
498
    ///   - buffer: The `ByteBuffer` to write.
499
    ///   - eventLoop: The `EventLoop` to create the returned `EventLoopFuture` from.
500
    /// - Returns: An `EventLoopFuture` which is fulfilled if the write was successful or fails on error.
501
    public func write(
502
        fileHandle: NIOFileHandle,
503
        toOffset: Int64,
504
        buffer: ByteBuffer,
505
        eventLoop: EventLoop
506
0
    ) -> EventLoopFuture<()> {
507
0
        self.write0(fileHandle: fileHandle, toOffset: toOffset, buffer: buffer, eventLoop: eventLoop)
508
0
    }
509
510
    private func write0(
511
        fileHandle: NIOFileHandle,
512
        toOffset: Int64?,
513
        buffer: ByteBuffer,
514
        eventLoop: EventLoop
515
0
    ) -> EventLoopFuture<()> {
516
0
        let byteCount = buffer.readableBytes
517
0
518
0
        guard byteCount > 0 else {
519
0
            return eventLoop.makeSucceededFuture(())
520
0
        }
521
0
522
0
        return self.threadPool.runIfActive(eventLoop: eventLoop) {
523
0
            try self.writeSync(fileHandle: fileHandle, byteCount: byteCount, toOffset: toOffset, buffer: buffer)
524
0
        }
525
0
    }
526
527
    private func writeSync(
528
        fileHandle: NIOFileHandle,
529
        byteCount: Int,
530
        toOffset: Int64?,
531
        buffer: ByteBuffer
532
0
    ) throws {
533
0
        var buf = buffer
534
0
535
0
        var offsetAccumulator: Int = 0
536
0
        repeat {
537
0
            let n = try buf.readWithUnsafeReadableBytes { ptr in
538
0
                precondition(ptr.count == byteCount - offsetAccumulator)
539
0
                let res: IOResult<ssize_t> = try fileHandle.withUnsafeFileDescriptor { descriptor in
540
0
                    if let toOffset = toOffset {
541
                        #if os(Windows)
542
                        return try Windows.pwrite(
543
                            descriptor: descriptor,
544
                            pointer: ptr.baseAddress!,
545
                            size: byteCount - offsetAccumulator,
546
                            offset: off_t(toOffset + Int64(offsetAccumulator))
547
                        )
548
                        #else
549
0
                        return try Posix.pwrite(
550
0
                            descriptor: descriptor,
551
0
                            pointer: ptr.baseAddress!,
552
0
                            size: byteCount - offsetAccumulator,
553
0
                            offset: off_t(toOffset + Int64(offsetAccumulator))
554
0
                        )
555
                        #endif
556
0
                    } else {
557
0
                        let result = try Posix.write(
558
0
                            descriptor: descriptor,
559
0
                            pointer: ptr.baseAddress!,
560
0
                            size: byteCount - offsetAccumulator
561
0
                        )
562
                        #if os(Windows)
563
                        return result.map { ssize_t($0) }
564
                        #else
565
0
                        return result
566
                        #endif
567
0
                    }
568
0
                }
569
0
                switch res {
570
0
                case .processed(let n):
571
0
                    assert(n >= 0, "write claims to have written a negative number of bytes \(n)")
572
0
                    return numericCast(n)
573
0
                case .wouldBlock:
574
0
                    throw Error.descriptorSetToNonBlocking
575
0
                }
576
0
            }
577
0
            offsetAccumulator += n
578
0
        } while offsetAccumulator < byteCount
579
0
    }
580
581
    /// Open the file at `path` for reading on a private thread pool which is separate from any `EventLoop` thread.
582
    ///
583
    /// This function will return (a future) of the `NIOFileHandle` associated with the file opened and a `FileRegion`
584
    /// comprising of the whole file. The caller must close the returned `NIOFileHandle` when it's no longer needed.
585
    ///
586
    /// - Note: The reason this returns the `NIOFileHandle` and the `FileRegion` is that both the opening of a file as well as the querying of its size are blocking.
587
    ///
588
    /// - Parameters:
589
    ///   - path: The path of the file to be opened for reading.
590
    ///   - eventLoop: The `EventLoop` on which the returned `EventLoopFuture` will fire.
591
    /// - Returns: An `EventLoopFuture` containing the `NIOFileHandle` and the `FileRegion` comprising the whole file.
592
    @available(
593
        *,
594
        deprecated,
595
        message:
596
            "Avoid using NIOFileHandle. The type is difficult to hold correctly, use NIOFileSystem as a replacement API."
597
    )
598
0
    public func openFile(path: String, eventLoop: EventLoop) -> EventLoopFuture<(NIOFileHandle, FileRegion)> {
599
0
        self.openFile(_deprecatedPath: path, eventLoop: eventLoop)
600
0
    }
601
602
    /// Open the file at `path` for reading on a private thread pool which is separate from any `EventLoop` thread.
603
    ///
604
    /// This function will return (a future) of the `NIOFileHandle` associated with the file opened and a `FileRegion`
605
    /// comprising of the whole file. The caller must close the returned `NIOFileHandle` when it's no longer needed.
606
    ///
607
    /// - Note: The reason this returns the `NIOFileHandle` and the `FileRegion` is that both the opening of a file as well as the querying of its size are blocking.
608
    ///
609
    /// - Parameters:
610
    ///   - path: The path of the file to be opened for reading.
611
    ///   - eventLoop: The `EventLoop` on which the returned `EventLoopFuture` will fire.
612
    /// - Returns: An `EventLoopFuture` containing the `NIOFileHandle` and the `FileRegion` comprising the whole file.
613
    public func openFile(
614
        _deprecatedPath path: String,
615
        eventLoop: EventLoop
616
0
    ) -> EventLoopFuture<(NIOFileHandle, FileRegion)> {
617
0
        self.threadPool.runIfActive(eventLoop: eventLoop) {
618
0
            let fh = try NIOFileHandle(_deprecatedPath: path)
619
0
            do {
620
0
                let fr = try FileRegion(fileHandle: fh)
621
0
                return (fh, fr)
622
0
            } catch {
623
0
                _ = try? fh.close()
624
0
                throw error
625
0
            }
626
0
        }
627
0
    }
628
629
    /// Open the file at `path` with specified access mode and POSIX flags on a private thread pool which is separate from any `EventLoop` thread.
630
    ///
631
    /// This function will return (a future) of the `NIOFileHandle` associated with the file opened.
632
    /// The caller must close the returned `NIOFileHandle` when it's no longer needed.
633
    ///
634
    /// - Parameters:
635
    ///   - path: The path of the file to be opened for writing.
636
    ///   - mode: File access mode.
637
    ///   - flags: Additional POSIX flags.
638
    ///   - eventLoop: The `EventLoop` on which the returned `EventLoopFuture` will fire.
639
    /// - Returns: An `EventLoopFuture` containing the `NIOFileHandle`.
640
    @available(
641
        *,
642
        deprecated,
643
        message:
644
            "Avoid using NonBlockingFileIO. The type is difficult to hold correctly, use NIOFileSystem as a replacement API."
645
    )
646
    public func openFile(
647
        path: String,
648
        mode: NIOFileHandle.Mode,
649
        flags: NIOFileHandle.Flags = .default,
650
        eventLoop: EventLoop
651
0
    ) -> EventLoopFuture<NIOFileHandle> {
652
0
        self.openFile(_deprecatedPath: path, mode: mode, flags: flags, eventLoop: eventLoop)
653
0
    }
654
655
    /// Open the file at `path` with specified access mode and POSIX flags on a private thread pool which is separate from any `EventLoop` thread.
656
    ///
657
    /// This function will return (a future) of the `NIOFileHandle` associated with the file opened.
658
    /// The caller must close the returned `NIOFileHandle` when it's no longer needed.
659
    ///
660
    /// - Parameters:
661
    ///   - path: The path of the file to be opened for writing.
662
    ///   - mode: File access mode.
663
    ///   - flags: Additional POSIX flags.
664
    ///   - eventLoop: The `EventLoop` on which the returned `EventLoopFuture` will fire.
665
    /// - Returns: An `EventLoopFuture` containing the `NIOFileHandle`.
666
    public func openFile(
667
        _deprecatedPath path: String,
668
        mode: NIOFileHandle.Mode,
669
        flags: NIOFileHandle.Flags = .default,
670
        eventLoop: EventLoop
671
0
    ) -> EventLoopFuture<NIOFileHandle> {
672
0
        self.threadPool.runIfActive(eventLoop: eventLoop) {
673
0
            try NIOFileHandle(_deprecatedPath: path, mode: mode, flags: flags)
674
0
        }
675
0
    }
676
677
    #if !os(Windows)
678
    /// Returns information about a file at `path` on a private thread pool which is separate from any `EventLoop` thread.
679
    ///
680
    /// - Note: If `path` is a symlink, information about the link, not the file it points to.
681
    ///
682
    /// - Parameters:
683
    ///   - path: The path of the file to get information about.
684
    ///   - eventLoop: The `EventLoop` on which the returned `EventLoopFuture` will fire.
685
    /// - Returns: An `EventLoopFuture` containing file information.
686
0
    public func lstat(path: String, eventLoop: EventLoop) -> EventLoopFuture<stat> {
687
0
        self.threadPool.runIfActive(eventLoop: eventLoop) {
688
0
            var s = stat()
689
0
            try Posix.lstat(pathname: path, outStat: &s)
690
0
            return s
691
0
        }
692
0
    }
693
694
    /// Creates a symbolic link to a  `destination` file  at `path` on a private thread pool which is separate from any `EventLoop` thread.
695
    ///
696
    /// - Parameters:
697
    ///   - path: The path of the link.
698
    ///   - destination: Target path where this link will point to.
699
    ///   - eventLoop: The `EventLoop` on which the returned `EventLoopFuture` will fire.
700
    /// - Returns: An `EventLoopFuture` which is fulfilled if the rename was successful or fails on error.
701
0
    public func symlink(path: String, to destination: String, eventLoop: EventLoop) -> EventLoopFuture<Void> {
702
0
        self.threadPool.runIfActive(eventLoop: eventLoop) {
703
0
            try Posix.symlink(pathname: path, destination: destination)
704
0
        }
705
0
    }
706
707
    /// Returns target of the symbolic link at `path` on a private thread pool which is separate from any `EventLoop` thread.
708
    ///
709
    /// - Parameters:
710
    ///   - path: The path of the link to read.
711
    ///   - eventLoop: The `EventLoop` on which the returned `EventLoopFuture` will fire.
712
    /// - Returns: An `EventLoopFuture` containing link target.
713
0
    public func readlink(path: String, eventLoop: EventLoop) -> EventLoopFuture<String> {
714
0
        self.threadPool.runIfActive(eventLoop: eventLoop) {
715
0
            let maxLength = Int(PATH_MAX)
716
0
            let pointer = UnsafeMutableBufferPointer<CChar>.allocate(capacity: maxLength)
717
0
            defer {
718
0
                pointer.deallocate()
719
0
            }
720
0
            let length = try Posix.readlink(pathname: path, outPath: pointer.baseAddress!, outPathSize: maxLength)
721
0
            return String(decoding: UnsafeRawBufferPointer(pointer).prefix(length), as: UTF8.self)
722
0
        }
723
0
    }
724
725
    /// Removes symbolic link at `path` on a private thread pool which is separate from any `EventLoop` thread.
726
    ///
727
    /// - Parameters:
728
    ///   - path: The path of the link to remove.
729
    ///   - eventLoop: The `EventLoop` on which the returned `EventLoopFuture` will fire.
730
    /// - Returns: An `EventLoopFuture` which is fulfilled if the rename was successful or fails on error.
731
0
    public func unlink(path: String, eventLoop: EventLoop) -> EventLoopFuture<Void> {
732
0
        self.threadPool.runIfActive(eventLoop: eventLoop) {
733
0
            try Posix.unlink(pathname: path)
734
0
        }
735
0
    }
736
737
0
    private func createDirectory0(_ path: String, mode: NIOPOSIXFileMode) throws {
738
0
        let pathView = path.utf8
739
0
        if pathView.isEmpty {
740
0
            return
741
0
        }
742
0
743
0
        // Fail fast if not a directory or file exists
744
0
        do {
745
0
            var s = stat()
746
0
            try Posix.stat(pathname: path, outStat: &s)
747
0
            if (S_IFMT & s.st_mode) == S_IFDIR {
748
0
                return
749
0
            }
750
0
            throw IOError(errnoCode: ENOTDIR, reason: "Not a directory")
751
0
        } catch let error as IOError where error.errnoCode == ENOENT {
752
0
            // if directory does not exist we can proceed with creating it
753
0
        }
754
0
755
0
        // Slow path, check that all intermediate directories exist recursively
756
0
757
0
        // Trim any trailing path separators
758
0
        var index = pathView.index(before: pathView.endIndex)
759
0
        let pathSeparator = UInt8(ascii: "/")
760
0
        while index != pathView.startIndex && pathView[index] == pathSeparator {
761
0
            index = pathView.index(before: index)
762
0
        }
763
0
764
0
        // Find first non-trailing path separator if it exists
765
0
        while index != pathView.startIndex && pathView[index] != pathSeparator {
766
0
            index = pathView.index(before: index)
767
0
        }
768
0
769
0
        // If non-trailing path separator is found, create parent directory
770
0
        if index > pathView.startIndex {
771
0
            try self.createDirectory0(String(Substring(pathView.prefix(upTo: index))), mode: mode)
772
0
        }
773
0
774
0
        do {
775
0
            try Posix.mkdir(pathname: path, mode: mode)
776
0
        } catch {
777
0
            // If user tries to create a path like `/some/path/.` it may fail, as path will be created
778
0
            // by the recursive call. Checks if directory exists and re-throw the error if it does not
779
0
            do {
780
0
                var s = stat()
781
0
                try Posix.lstat(pathname: path, outStat: &s)
782
0
                if (S_IFMT & s.st_mode) == S_IFDIR {
783
0
                    return
784
0
                }
785
0
            } catch {
786
0
                // fallthrough
787
0
            }
788
0
            throw error
789
0
        }
790
0
    }
791
792
    /// Creates directory at `path` on a private thread pool which is separate from any `EventLoop` thread.
793
    ///
794
    /// - Parameters:
795
    ///   - path: The path of the directory to be created.
796
    ///   - createIntermediates: Whether intermediate directories should be created.
797
    ///   - mode: POSIX file mode.
798
    ///   - eventLoop: The `EventLoop` on which the returned `EventLoopFuture` will fire.
799
    /// - Returns: An `EventLoopFuture` which is fulfilled if the rename was successful or fails on error.
800
    public func createDirectory(
801
        path: String,
802
        withIntermediateDirectories createIntermediates: Bool = false,
803
        mode: NIOPOSIXFileMode,
804
        eventLoop: EventLoop
805
0
    ) -> EventLoopFuture<Void> {
806
0
        self.threadPool.runIfActive(eventLoop: eventLoop) {
807
0
            if createIntermediates {
808
                #if canImport(Darwin)
809
                try Posix.mkpath_np(pathname: path, mode: mode)
810
                #else
811
0
                try self.createDirectory0(path, mode: mode)
812
                #endif
813
0
            } else {
814
0
                try Posix.mkdir(pathname: path, mode: mode)
815
0
            }
816
0
        }
817
0
    }
818
819
    /// List contents of the directory at `path` on a private thread pool which is separate from any `EventLoop` thread.
820
    ///
821
    /// - Parameters:
822
    ///   - path: The path of the directory to list the content of.
823
    ///   - eventLoop: The `EventLoop` on which the returned `EventLoopFuture` will fire.
824
    /// - Returns: An `EventLoopFuture` containing the directory entries.
825
0
    public func listDirectory(path: String, eventLoop: EventLoop) -> EventLoopFuture<[NIODirectoryEntry]> {
826
0
        self.threadPool.runIfActive(eventLoop: eventLoop) {
827
0
            let dir = try Posix.opendir(pathname: path)
828
0
            var entries: [NIODirectoryEntry] = []
829
0
            do {
830
0
                while let entry = try Posix.readdir(dir: dir) {
831
0
                    let name = withUnsafeBytes(of: entry.pointee.d_name) { pointer -> String in
832
0
                        let ptr = pointer.baseAddress!.assumingMemoryBound(to: CChar.self)
833
0
                        return String(cString: ptr)
834
0
                    }
835
                    #if os(OpenBSD)
836
                    let ino = entry.pointee.d_fileno
837
                    #else
838
0
                    let ino = entry.pointee.d_ino
839
                    #endif
840
0
                    entries.append(
841
0
                        NIODirectoryEntry(ino: UInt64(ino), type: entry.pointee.d_type, name: name)
842
0
                    )
843
0
                }
844
0
                try? Posix.closedir(dir: dir)
845
0
            } catch {
846
0
                try? Posix.closedir(dir: dir)
847
0
                throw error
848
0
            }
849
0
            return entries
850
0
        }
851
0
    }
852
853
    /// Renames the file at `path` to `newName` on a private thread pool which is separate from any `EventLoop` thread.
854
    ///
855
    /// - Parameters:
856
    ///   - path: The path of the file to be renamed.
857
    ///   - newName: New file name.
858
    ///   - eventLoop: The `EventLoop` on which the returned `EventLoopFuture` will fire.
859
    /// - Returns: An `EventLoopFuture` which is fulfilled if the rename was successful or fails on error.
860
0
    public func rename(path: String, newName: String, eventLoop: EventLoop) -> EventLoopFuture<Void> {
861
0
        self.threadPool.runIfActive(eventLoop: eventLoop) {
862
0
            try Posix.rename(pathname: path, newName: newName)
863
0
        }
864
0
    }
865
866
    /// Removes the file at `path` on a private thread pool which is separate from any `EventLoop` thread.
867
    ///
868
    /// - Parameters:
869
    ///   - path: The path of the file to be removed.
870
    ///   - eventLoop: The `EventLoop` on which the returned `EventLoopFuture` will fire.
871
    /// - Returns: An `EventLoopFuture` which is fulfilled if the remove was successful or fails on error.
872
0
    public func remove(path: String, eventLoop: EventLoop) -> EventLoopFuture<Void> {
873
0
        self.threadPool.runIfActive(eventLoop: eventLoop) {
874
0
            try Posix.remove(pathname: path)
875
0
        }
876
0
    }
877
    #endif
878
}
879
880
#if !os(Windows)
881
/// A `NIODirectoryEntry` represents a single directory entry.
882
public struct NIODirectoryEntry: Hashable, Sendable {
883
    // File number of entry
884
    public var ino: UInt64
885
    // File type
886
    public var type: UInt8
887
    // File name
888
    public var name: String
889
890
0
    public init(ino: UInt64, type: UInt8, name: String) {
891
0
        self.ino = ino
892
0
        self.type = type
893
0
        self.name = name
894
0
    }
895
}
896
#endif
897
898
extension NonBlockingFileIO {
899
    /// Read a `FileRegion` in ``NonBlockingFileIO``'s private thread pool.
900
    ///
901
    /// The returned `ByteBuffer` will not have less than the minimum of `fileRegion.readableBytes` and `UInt32.max` unless we hit
902
    /// end-of-file in which case the `ByteBuffer` will contain the bytes available to read.
903
    ///
904
    /// This method will not use the file descriptor's seek pointer which means there is no danger of reading from the
905
    /// same `FileRegion` in multiple threads.
906
    ///
907
    /// - Note: Only use this function for small enough `FileRegion`s as it will need to allocate enough memory to hold `fileRegion.readableBytes` bytes.
908
    /// - Note: In most cases you should prefer one of the `readChunked` functions.
909
    ///
910
    /// - Parameters:
911
    ///   - fileRegion: The file region to read.
912
    ///   - allocator: A `ByteBufferAllocator` used to allocate space for the returned `ByteBuffer`.
913
    /// - Returns: ByteBuffer.
914
    @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
915
0
    public func read(fileRegion: FileRegion, allocator: ByteBufferAllocator) async throws -> ByteBuffer {
916
0
        let readableBytes = fileRegion.readableBytes
917
0
        return try await self.read(
918
0
            fileHandle: fileRegion.fileHandle,
919
0
            fromOffset: Int64(fileRegion.readerIndex),
920
0
            byteCount: readableBytes,
921
0
            allocator: allocator
922
0
        )
923
0
    }
924
925
    /// Read `byteCount` bytes from `fileHandle` in ``NonBlockingFileIO``'s private thread pool.
926
    ///
927
    /// The returned `ByteBuffer` will not have less than `byteCount` bytes unless we hit end-of-file in which
928
    /// case the `ByteBuffer` will contain the bytes available to read.
929
    ///
930
    /// - Note: Only use this function for small enough `byteCount`s as it will need to allocate enough memory to hold `byteCount` bytes.
931
    /// - Note: ``read(fileRegion:allocator:eventLoop:)`` should be preferred as it uses `FileRegion` object instead of
932
    ///         raw `NIOFileHandle`s. In case you do want to use raw `NIOFileHandle`s,
933
    ///         please consider using ``read(fileHandle:fromOffset:byteCount:allocator:eventLoop:)``
934
    ///         because it doesn't use the file descriptor's seek pointer (which may be shared with other file
935
    ///         descriptors and even across processes.)
936
    ///
937
    /// - Parameters:
938
    ///   - fileHandle: The `NIOFileHandle` to read.
939
    ///   - byteCount: The number of bytes to read from `fileHandle`.
940
    ///   - allocator: A `ByteBufferAllocator` used to allocate space for the returned `ByteBuffer`.
941
    /// - Returns: ByteBuffer.
942
    @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
943
    public func read(
944
        fileHandle: NIOFileHandle,
945
        byteCount: Int,
946
        allocator: ByteBufferAllocator
947
0
    ) async throws -> ByteBuffer {
948
0
        try await self.read0(
949
0
            fileHandle: fileHandle,
950
0
            fromOffset: nil,
951
0
            byteCount: byteCount,
952
0
            allocator: allocator
953
0
        )
954
0
    }
955
956
    /// Read `byteCount` bytes starting at `fileOffset` from `fileHandle` in ``NonBlockingFileIO``'s private thread pool
957
    ///.
958
    ///
959
    /// The returned `ByteBuffer` will not have less than `byteCount` bytes unless we hit end-of-file in which
960
    /// case the `ByteBuffer` will contain the bytes available to read.
961
    ///
962
    /// This method will not use the file descriptor's seek pointer which means there is no danger of reading from the
963
    /// same `fileHandle` in multiple threads.
964
    ///
965
    /// - Note: Only use this function for small enough `byteCount`s as it will need to allocate enough memory to hold `byteCount` bytes.
966
    /// - Note: ``read(fileRegion:allocator:eventLoop:)`` should be preferred as it uses `FileRegion` object instead of raw `NIOFileHandle`s.
967
    ///
968
    /// - Parameters:
969
    ///   - fileHandle: The `NIOFileHandle` to read.
970
    ///   - fileOffset: The offset to read from.
971
    ///   - byteCount: The number of bytes to read from `fileHandle`.
972
    ///   - allocator: A `ByteBufferAllocator` used to allocate space for the returned `ByteBuffer`.
973
    /// - Returns: ByteBuffer.
974
    @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
975
    public func read(
976
        fileHandle: NIOFileHandle,
977
        fromOffset fileOffset: Int64,
978
        byteCount: Int,
979
        allocator: ByteBufferAllocator
980
0
    ) async throws -> ByteBuffer {
981
0
        try await self.read0(
982
0
            fileHandle: fileHandle,
983
0
            fromOffset: fileOffset,
984
0
            byteCount: byteCount,
985
0
            allocator: allocator
986
0
        )
987
0
    }
988
989
    @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
990
    private func read0(
991
        fileHandle: NIOFileHandle,
992
        fromOffset: Int64?,  // > 2 GB offset is reasonable on 32-bit systems
993
        byteCount rawByteCount: Int,
994
        allocator: ByteBufferAllocator
995
0
    ) async throws -> ByteBuffer {
996
0
        guard rawByteCount > 0 else {
997
0
            return allocator.buffer(capacity: 0)
998
0
        }
999
0
        let byteCount = rawByteCount < Int32.max ? rawByteCount : size_t(Int32.max)
1000
0
1001
0
        return try await self.threadPool.runIfActive { () -> ByteBuffer in
1002
0
            try self.readSync(
1003
0
                fileHandle: fileHandle,
1004
0
                fromOffset: fromOffset,
1005
0
                byteCount: byteCount,
1006
0
                allocator: allocator
1007
0
            )
1008
0
        }
1009
0
    }
1010
1011
    /// Changes the file size of `fileHandle` to `size`.
1012
    ///
1013
    /// If `size` is smaller than the current file size, the remaining bytes will be truncated and are lost. If `size`
1014
    /// is larger than the current file size, the gap will be filled with zero bytes.
1015
    ///
1016
    /// - Parameters:
1017
    ///   - fileHandle: The `NIOFileHandle` to write to.
1018
    ///   - size: The new file size in bytes to write.
1019
    @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
1020
    public func changeFileSize(
1021
        fileHandle: NIOFileHandle,
1022
        size: Int64
1023
0
    ) async throws {
1024
0
        try await self.threadPool.runIfActive {
1025
0
            try fileHandle.withUnsafeFileDescriptor { descriptor -> Void in
1026
0
                try Posix.ftruncate(descriptor: descriptor, size: off_t(size))
1027
0
            }
1028
0
        }
1029
0
    }
1030
1031
    /// Returns the length of the file associated with `fileHandle`.
1032
    ///
1033
    /// - Parameters:
1034
    ///   - fileHandle: The `NIOFileHandle` to read from.
1035
    @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
1036
0
    public func readFileSize(fileHandle: NIOFileHandle) async throws -> Int64 {
1037
0
        try await self.threadPool.runIfActive {
1038
0
            try fileHandle.withUnsafeFileDescriptor { descriptor in
1039
0
                let curr = try Posix.lseek(descriptor: descriptor, offset: 0, whence: SEEK_CUR)
1040
0
                let eof = try Posix.lseek(descriptor: descriptor, offset: 0, whence: SEEK_END)
1041
0
                try Posix.lseek(descriptor: descriptor, offset: curr, whence: SEEK_SET)
1042
0
                return Int64(eof)
1043
0
            }
1044
0
        }
1045
0
    }
1046
1047
    /// Write `buffer` to `fileHandle` in ``NonBlockingFileIO``'s private thread pool.
1048
    ///
1049
    /// - Parameters:
1050
    ///   - fileHandle: The `NIOFileHandle` to write to.
1051
    ///   - buffer: The `ByteBuffer` to write.
1052
    @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
1053
    public func write(
1054
        fileHandle: NIOFileHandle,
1055
        buffer: ByteBuffer
1056
0
    ) async throws {
1057
0
        try await self.write0(fileHandle: fileHandle, toOffset: nil, buffer: buffer)
1058
0
    }
1059
1060
    /// Write `buffer` starting from `toOffset` to `fileHandle` in ``NonBlockingFileIO``'s private thread pool.
1061
    ///
1062
    /// - Parameters:
1063
    ///   - fileHandle: The `NIOFileHandle` to write to.
1064
    ///   - toOffset: The file offset to write to.
1065
    ///   - buffer: The `ByteBuffer` to write.
1066
    @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
1067
    public func write(
1068
        fileHandle: NIOFileHandle,
1069
        toOffset: Int64,
1070
        buffer: ByteBuffer
1071
0
    ) async throws {
1072
0
        try await self.write0(fileHandle: fileHandle, toOffset: toOffset, buffer: buffer)
1073
0
    }
1074
1075
    @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
1076
    private func write0(
1077
        fileHandle: NIOFileHandle,
1078
        toOffset: Int64?,
1079
        buffer: ByteBuffer
1080
0
    ) async throws {
1081
0
        let byteCount = buffer.readableBytes
1082
0
1083
0
        guard byteCount > 0 else {
1084
0
            return
1085
0
        }
1086
0
1087
0
        return try await self.threadPool.runIfActive {
1088
0
            try self.writeSync(fileHandle: fileHandle, byteCount: byteCount, toOffset: toOffset, buffer: buffer)
1089
0
        }
1090
0
    }
1091
1092
    /// Open file at `path` and query its size on a private thread pool, run an operation given
1093
    /// the resulting file region and then close the file handle.
1094
    ///
1095
    /// The will return the result of the operation.
1096
    ///
1097
    /// - Note: This function opens a file and queries it size which are both blocking operations
1098
    ///
1099
    /// - Parameters:
1100
    ///   - path: The path of the file to be opened for reading.
1101
    ///   - body: operation to run with file handle and region
1102
    /// - Returns: return value of operation
1103
    @available(
1104
        *,
1105
        deprecated,
1106
        message:
1107
            "Avoid using NonBlockingFileIO. The API is difficult to hold correctly, use NIOFileSystem as a replacement API."
1108
    )
1109
    @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
1110
    public func withFileRegion<Result>(
1111
        path: String,
1112
        _ body: (_ fileRegion: FileRegion) async throws -> Result
1113
0
    ) async throws -> Result {
1114
0
        try await self.withFileRegion(_deprecatedPath: path, body)
1115
0
    }
1116
1117
    @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
1118
    public func withFileRegion<Result>(
1119
        _deprecatedPath path: String,
1120
        _ body: (_ fileRegion: FileRegion) async throws -> Result
1121
0
    ) async throws -> Result {
1122
0
        let fileRegion = try await self.threadPool.runIfActive {
1123
0
            let fh = try NIOFileHandle(_deprecatedPath: path)
1124
0
            do {
1125
0
                return try FileRegion(fileHandle: fh)
1126
0
            } catch {
1127
0
                _ = try? fh.close()
1128
0
                throw error
1129
0
            }
1130
0
        }
1131
0
        let result: Result
1132
0
        do {
1133
0
            result = try await body(fileRegion)
1134
0
        } catch {
1135
0
            try fileRegion.fileHandle.close()
1136
0
            throw error
1137
0
        }
1138
0
        try fileRegion.fileHandle.close()
1139
0
        return result
1140
0
    }
1141
1142
    /// Open file at `path` on a private thread pool, run an operation given the file handle and then close the file handle.
1143
    ///
1144
    /// This function will return the result of the operation.
1145
    ///
1146
    /// - Parameters:
1147
    ///   - path: The path of the file to be opened for writing.
1148
    ///   - mode: File access mode.
1149
    ///   - flags: Additional POSIX flags.
1150
    ///   - body: operation to run with the file handle
1151
    /// - Returns: return value of operation
1152
    @available(
1153
        *,
1154
        deprecated,
1155
        message:
1156
            "Avoid using NonBlockingFileIO. The API is difficult to hold correctly, use NIOFileSystem as a replacement API."
1157
    )
1158
    @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
1159
    public func withFileHandle<Result>(
1160
        path: String,
1161
        mode: NIOFileHandle.Mode,
1162
        flags: NIOFileHandle.Flags = .default,
1163
        _ body: (NIOFileHandle) async throws -> Result
1164
0
    ) async throws -> Result {
1165
0
        try await self.withFileHandle(_deprecatedPath: path, mode: mode, flags: flags, body)
1166
0
    }
1167
1168
    @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
1169
    public func withFileHandle<Result>(
1170
        _deprecatedPath path: String,
1171
        mode: NIOFileHandle.Mode,
1172
        flags: NIOFileHandle.Flags = .default,
1173
        _ body: (NIOFileHandle) async throws -> Result
1174
0
    ) async throws -> Result {
1175
0
        let fileHandle = try await self.threadPool.runIfActive {
1176
0
            try NIOFileHandle(_deprecatedPath: path, mode: mode, flags: flags)
1177
0
        }
1178
0
        let result: Result
1179
0
        do {
1180
0
            result = try await body(fileHandle)
1181
0
        } catch {
1182
0
            try fileHandle.close()
1183
0
            throw error
1184
0
        }
1185
0
        try fileHandle.close()
1186
0
        return result
1187
0
    }
1188
1189
    #if !os(Windows)
1190
1191
    /// Returns information about a file at `path` on a private thread pool.
1192
    ///
1193
    /// - Note: If `path` is a symlink, information about the link, not the file it points to.
1194
    ///
1195
    /// - Parameters:
1196
    ///   - path: The path of the file to get information about.
1197
    /// - Returns: file information.
1198
    @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
1199
0
    public func lstat(path: String) async throws -> stat {
1200
0
        try await self.threadPool.runIfActive {
1201
0
            var s = stat()
1202
0
            try Posix.lstat(pathname: path, outStat: &s)
1203
0
            return s
1204
0
        }
1205
0
    }
1206
1207
    /// Creates a symbolic link to a  `destination` file  at `path` on a private thread pool.
1208
    ///
1209
    /// - Parameters:
1210
    ///   - path: The path of the link.
1211
    ///   - destination: Target path where this link will point to.
1212
    @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
1213
0
    public func symlink(path: String, to destination: String) async throws {
1214
0
        try await self.threadPool.runIfActive {
1215
0
            try Posix.symlink(pathname: path, destination: destination)
1216
0
        }
1217
0
    }
1218
1219
    /// Returns target of the symbolic link at `path` on a private thread pool.
1220
    ///
1221
    /// - Parameters:
1222
    ///   - path: The path of the link to read.
1223
    /// - Returns: link target.
1224
    @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
1225
0
    public func readlink(path: String) async throws -> String {
1226
0
        try await self.threadPool.runIfActive {
1227
0
            let maxLength = Int(PATH_MAX)
1228
0
            let pointer = UnsafeMutableBufferPointer<CChar>.allocate(capacity: maxLength)
1229
0
            defer {
1230
0
                pointer.deallocate()
1231
0
            }
1232
0
            let length = try Posix.readlink(pathname: path, outPath: pointer.baseAddress!, outPathSize: maxLength)
1233
0
            return String(decoding: UnsafeRawBufferPointer(pointer).prefix(length), as: UTF8.self)
1234
0
        }
1235
0
    }
1236
1237
    /// Removes symbolic link at `path` on a private thread pool which is separate from any `EventLoop` thread.
1238
    ///
1239
    /// - Parameters:
1240
    ///   - path: The path of the link to remove.
1241
    @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
1242
0
    public func unlink(path: String) async throws {
1243
0
        try await self.threadPool.runIfActive {
1244
0
            try Posix.unlink(pathname: path)
1245
0
        }
1246
0
    }
1247
1248
    /// Creates directory at `path` on a private thread pool.
1249
    ///
1250
    /// - Parameters:
1251
    ///   - path: The path of the directory to be created.
1252
    ///   - createIntermediates: Whether intermediate directories should be created.
1253
    ///   - mode: POSIX file mode.
1254
    @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
1255
    public func createDirectory(
1256
        path: String,
1257
        withIntermediateDirectories createIntermediates: Bool = false,
1258
        mode: NIOPOSIXFileMode
1259
0
    ) async throws {
1260
0
        try await self.threadPool.runIfActive {
1261
0
            if createIntermediates {
1262
                #if canImport(Darwin)
1263
                try Posix.mkpath_np(pathname: path, mode: mode)
1264
                #else
1265
0
                try self.createDirectory0(path, mode: mode)
1266
                #endif
1267
0
            } else {
1268
0
                try Posix.mkdir(pathname: path, mode: mode)
1269
0
            }
1270
0
        }
1271
0
    }
1272
1273
    /// List contents of the directory at `path` on a private thread pool.
1274
    ///
1275
    /// - Parameters:
1276
    ///   - path: The path of the directory to list the content of.
1277
    /// - Returns: The directory entries.
1278
    @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
1279
0
    public func listDirectory(path: String) async throws -> [NIODirectoryEntry] {
1280
0
        try await self.threadPool.runIfActive {
1281
0
            let dir = try Posix.opendir(pathname: path)
1282
0
            var entries: [NIODirectoryEntry] = []
1283
0
            do {
1284
0
                while let entry = try Posix.readdir(dir: dir) {
1285
0
                    let name = withUnsafeBytes(of: entry.pointee.d_name) { pointer -> String in
1286
0
                        let ptr = pointer.baseAddress!.assumingMemoryBound(to: CChar.self)
1287
0
                        return String(cString: ptr)
1288
0
                    }
1289
                    #if os(OpenBSD)
1290
                    let ino = entry.pointee.d_fileno
1291
                    #else
1292
0
                    let ino = entry.pointee.d_ino
1293
                    #endif
1294
0
                    entries.append(
1295
0
                        NIODirectoryEntry(ino: UInt64(ino), type: entry.pointee.d_type, name: name)
1296
0
                    )
1297
0
                }
1298
0
                try? Posix.closedir(dir: dir)
1299
0
            } catch {
1300
0
                try? Posix.closedir(dir: dir)
1301
0
                throw error
1302
0
            }
1303
0
            return entries
1304
0
        }
1305
0
    }
1306
1307
    /// Renames the file at `path` to `newName` on a private thread pool.
1308
    ///
1309
    /// - Parameters:
1310
    ///   - path: The path of the file to be renamed.
1311
    ///   - newName: New file name.
1312
    @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
1313
0
    public func rename(path: String, newName: String) async throws {
1314
0
        try await self.threadPool.runIfActive {
1315
0
            try Posix.rename(pathname: path, newName: newName)
1316
0
        }
1317
0
    }
1318
1319
    /// Removes the file at `path` on a private thread pool.
1320
    ///
1321
    /// - Parameters:
1322
    ///   - path: The path of the file to be removed.
1323
    @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
1324
0
    public func remove(path: String) async throws {
1325
0
        try await self.threadPool.runIfActive {
1326
0
            try Posix.remove(pathname: path)
1327
0
        }
1328
0
    }
1329
    #endif
1330
}
1331
#endif  // !os(WASI)