Coverage Report

Created: 2025-09-08 06:42

/src/swift-nio/Sources/NIOCore/FileHandle.swift
Line
Count
Source (jump to first uncovered line)
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
import Atomics
16
17
#if os(Windows)
18
import ucrt
19
#elseif canImport(Darwin)
20
import Darwin
21
#elseif canImport(Glibc)
22
@preconcurrency import Glibc
23
#elseif canImport(Musl)
24
@preconcurrency import Musl
25
#elseif canImport(Android)
26
@preconcurrency import Android
27
#elseif canImport(WASILibc)
28
@preconcurrency import WASILibc
29
import CNIOWASI
30
#else
31
#error("The File Handle module was unable to identify your C library.")
32
#endif
33
34
#if os(Windows)
35
public typealias NIOPOSIXFileMode = CInt
36
#else
37
public typealias NIOPOSIXFileMode = mode_t
38
#endif
39
40
#if arch(x86_64) || arch(arm64)
41
// 64 bit architectures
42
typealias OneUInt32 = UInt32
43
typealias TwoUInt32s = UInt64
44
45
// Now we need to make `UInt64` match `DoubleWord`'s API but we can't use a custom
46
// type because we need special support by the `swift-atomics` package.
47
extension UInt64 {
48
0
    fileprivate init(first: UInt32, second: UInt32) {
49
0
        self = UInt64(first) << 32 | UInt64(second)
50
0
    }
51
52
    fileprivate var first: UInt32 {
53
0
        get {
54
0
            UInt32(truncatingIfNeeded: self >> 32)
55
0
        }
56
0
        set {
57
0
            self = (UInt64(newValue) << 32) | UInt64(self.second)
58
0
        }
59
    }
60
61
    fileprivate var second: UInt32 {
62
0
        get {
63
0
            UInt32(truncatingIfNeeded: self & 0xff_ff_ff_ff)
64
0
        }
65
0
        set {
66
0
            self = (UInt64(self.first) << 32) | UInt64(newValue)
67
0
        }
68
    }
69
}
70
#elseif arch(arm) || arch(i386) || arch(arm64_32) || arch(wasm32)
71
// 32 bit architectures
72
// Note: for testing purposes you can also use these defines for 64 bit platforms, they'll just consume twice as
73
// much space, nothing else will go bad.
74
typealias OneUInt32 = UInt
75
typealias TwoUInt32s = DoubleWord
76
#else
77
#error("Unknown architecture")
78
#endif
79
80
internal struct FileDescriptorState {
81
    private static let closedValue: OneUInt32 = 0xdead
82
    private static let inUseValue: OneUInt32 = 0xbeef
83
    private static let openValue: OneUInt32 = 0xcafe
84
    internal var rawValue: TwoUInt32s
85
86
0
    internal init(rawValue: TwoUInt32s) {
87
0
        self.rawValue = rawValue
88
0
    }
89
90
0
    internal init(descriptor: CInt) {
91
0
        self.rawValue = TwoUInt32s(
92
0
            first: .init(truncatingIfNeeded: CUnsignedInt(bitPattern: descriptor)),
93
0
            second: Self.openValue
94
0
        )
95
0
    }
96
97
    internal var descriptor: CInt {
98
0
        get {
99
0
            CInt(bitPattern: UInt32(truncatingIfNeeded: self.rawValue.first))
100
0
        }
101
0
        set {
102
0
            self.rawValue.first = .init(truncatingIfNeeded: CUnsignedInt(bitPattern: newValue))
103
0
        }
104
    }
105
106
0
    internal var isOpen: Bool {
107
0
        self.rawValue.second == Self.openValue
108
0
    }
109
110
0
    internal var isInUse: Bool {
111
0
        self.rawValue.second == Self.inUseValue
112
0
    }
113
114
0
    internal var isClosed: Bool {
115
0
        self.rawValue.second == Self.closedValue
116
0
    }
117
118
0
    mutating func close() {
119
0
        assert(self.isOpen)
120
0
        self.rawValue.second = Self.closedValue
121
0
    }
122
123
0
    mutating func markInUse() {
124
0
        assert(self.isOpen)
125
0
        self.rawValue.second = Self.inUseValue
126
0
    }
127
128
0
    mutating func markNotInUse() {
129
0
        assert(self.rawValue.second == Self.inUseValue)
130
0
        self.rawValue.second = Self.openValue
131
0
    }
132
}
133
134
/// Deprecated. `NIOFileHandle` is a handle to an open file descriptor.
135
///
136
/// - warning: The `NIOFileHandle` API is deprecated, do not use going forward. It's not marked as `deprecated` yet such
137
///            that users don't get the deprecation warnings affecting their APIs everywhere. For file I/O, please use
138
///            the `NIOFileSystem` API.
139
///
140
/// When creating a `NIOFileHandle` it takes ownership of the underlying file descriptor. When a `NIOFileHandle` is no longer
141
/// needed you must `close` it or take back ownership of the file descriptor using `takeDescriptorOwnership`.
142
///
143
/// - Note: One underlying file descriptor should usually be managed by one `NIOFileHandle` only.
144
///
145
/// - warning: Failing to manage the lifetime of a `NIOFileHandle` correctly will result in undefined behaviour.
146
///
147
/// - Note: As of SwiftNIO 2.77.0, `NIOFileHandle` objects are are thread-safe and enforce singular access. If you access the same `NIOFileHandle`
148
///         multiple times, it will throw `IOError(errorCode: EBUSY)` for the second access.
149
public final class NIOFileHandle: FileDescriptor & Sendable {
150
    private static let descriptorClosed: CInt = CInt.min
151
    private let descriptor: UnsafeAtomic<TwoUInt32s>
152
153
0
    public var isOpen: Bool {
154
0
        FileDescriptorState(
155
0
            rawValue: self.descriptor.load(ordering: .sequentiallyConsistent)
156
0
        ).isOpen
157
0
    }
158
159
    private static func interpretDescriptorValueThrowIfInUseOrNotOpen(
160
        _ descriptor: TwoUInt32s
161
0
    ) throws -> FileDescriptorState {
162
0
        let descriptorState = FileDescriptorState(rawValue: descriptor)
163
0
        if descriptorState.isOpen {
164
0
            return descriptorState
165
0
        } else if descriptorState.isClosed {
166
0
            throw IOError(errnoCode: EBADF, reason: "can't close file (as it's not open anymore).")
167
0
        } else {
168
0
            throw IOError(errnoCode: EBUSY, reason: "file descriptor currently in use")
169
0
        }
170
0
    }
171
172
0
    private func peekAtDescriptorIfOpen() throws -> FileDescriptorState {
173
0
        let descriptor = self.descriptor.load(ordering: .relaxed)
174
0
        return try Self.interpretDescriptorValueThrowIfInUseOrNotOpen(descriptor)
175
0
    }
176
177
    /// Create a `NIOFileHandle` taking ownership of `descriptor`. You must call `NIOFileHandle.close` or `NIOFileHandle.takeDescriptorOwnership` before
178
    /// this object can be safely released.
179
    @available(
180
        *,
181
        deprecated,
182
        message: """
183
            Avoid using NIOFileHandle. The type is difficult to hold correctly, \
184
            use NIOFileSystem as a replacement API.
185
            """
186
    )
187
0
    public convenience init(descriptor: CInt) {
188
0
        self.init(_deprecatedTakingOwnershipOfDescriptor: descriptor)
189
0
    }
190
191
    /// Create a `NIOFileHandle` taking ownership of `descriptor`. You must call `NIOFileHandle.close` or `NIOFileHandle.takeDescriptorOwnership` before
192
    /// this object can be safely released.
193
0
    public init(_deprecatedTakingOwnershipOfDescriptor descriptor: CInt) {
194
0
        self.descriptor = UnsafeAtomic.create(FileDescriptorState(descriptor: descriptor).rawValue)
195
0
    }
196
197
0
    deinit {
198
0
        assert(
199
0
            !self.isOpen,
200
0
            "leaked open NIOFileHandle(descriptor: \(self.descriptor)). Call `close()` to close or `takeDescriptorOwnership()` to take ownership and close by some other means."
201
0
        )
202
0
        self.descriptor.destroy()
203
0
    }
204
205
    #if !os(WASI)
206
    /// Duplicates this `NIOFileHandle`. This means that a new `NIOFileHandle` object with a new underlying file descriptor
207
    /// is returned. The caller takes ownership of the returned `NIOFileHandle` and is responsible for closing it.
208
    ///
209
    /// - warning: The returned `NIOFileHandle` is not fully independent, the seek pointer is shared as documented by `dup(2)`.
210
    ///
211
    /// - Returns: A new `NIOFileHandle` with a fresh underlying file descriptor but shared seek pointer.
212
0
    public func duplicate() throws -> NIOFileHandle {
213
0
        try self.withUnsafeFileDescriptor { fd in
214
0
            NIOFileHandle(_deprecatedTakingOwnershipOfDescriptor: try SystemCalls.dup(descriptor: fd))
215
0
        }
216
0
    }
217
    #endif
218
219
0
    private func activateDescriptor(as descriptor: CInt) {
220
0
        let desired = FileDescriptorState(descriptor: descriptor)
221
0
        var expected = desired
222
0
        expected.markInUse()
223
0
        let (exchanged, original) = self.descriptor.compareExchange(
224
0
            expected: expected.rawValue,
225
0
            desired: desired.rawValue,
226
0
            ordering: .sequentiallyConsistent
227
0
        )
228
0
        guard exchanged || FileDescriptorState(rawValue: original).isClosed else {
229
0
            fatalError("bug in NIO (please report): NIOFileDescritor activate failed \(original)")
230
0
        }
231
0
    }
232
233
0
    private func deactivateDescriptor(toClosed: Bool) throws -> CInt {
234
0
        let peekedDescriptor = try self.peekAtDescriptorIfOpen()
235
0
        // Don't worry, the above is just opportunistic. If we lose the race, we re-check below --> `!exchanged`
236
0
        assert(peekedDescriptor.isOpen)
237
0
        var desired = peekedDescriptor
238
0
        if toClosed {
239
0
            desired.close()
240
0
        } else {
241
0
            desired.markInUse()
242
0
        }
243
0
        assert(desired.rawValue != peekedDescriptor.rawValue, "\(desired.rawValue) == \(peekedDescriptor.rawValue)")
244
0
        let (exchanged, originalDescriptor) = self.descriptor.compareExchange(
245
0
            expected: peekedDescriptor.rawValue,
246
0
            desired: desired.rawValue,
247
0
            ordering: .sequentiallyConsistent
248
0
        )
249
0
250
0
        if exchanged {
251
0
            assert(peekedDescriptor.rawValue == originalDescriptor)
252
0
            return peekedDescriptor.descriptor
253
0
        } else {
254
0
            // We lost the race above, so this _will_ throw (as we're not closed).
255
0
            let fauxDescriptor = try Self.interpretDescriptorValueThrowIfInUseOrNotOpen(originalDescriptor)
256
0
            // This is impossible, because there are only 4 options in which the exchange above can fail
257
0
            // 1. Descriptor already closed (would've thrown above)
258
0
            // 2. Descriptor in use (would've thrown above)
259
0
            // 3. Descriptor at illegal negative value (would've crashed above)
260
0
            // 4. Descriptor a different, positive value (this is where we're at) --> memory corruption, let's crash
261
0
            fatalError(
262
0
                """
263
0
                bug in NIO (please report): \
264
0
                NIOFileDescriptor illegal state \
265
0
                (\(peekedDescriptor), \(originalDescriptor), \(fauxDescriptor))")
266
0
                """
267
0
            )
268
0
        }
269
0
    }
270
271
    /// Take the ownership of the underlying file descriptor. This is similar to `close()` but the underlying file
272
    /// descriptor remains open. The caller is responsible for closing the file descriptor by some other means.
273
    ///
274
    /// After calling this, the `NIOFileHandle` cannot be used for anything else and all the operations will throw.
275
    ///
276
    /// - Returns: The underlying file descriptor, now owned by the caller.
277
0
    public func takeDescriptorOwnership() throws -> CInt {
278
0
        try self.deactivateDescriptor(toClosed: true)
279
0
    }
280
281
0
    public func close() throws {
282
0
        let descriptor = try self.deactivateDescriptor(toClosed: true)
283
0
        try SystemCalls.close(descriptor: descriptor)
284
0
    }
285
286
0
    public func withUnsafeFileDescriptor<T>(_ body: (CInt) throws -> T) throws -> T {
287
0
        let descriptor = try self.deactivateDescriptor(toClosed: false)
288
0
        defer {
289
0
            self.activateDescriptor(as: descriptor)
290
0
        }
291
0
        return try body(descriptor)
292
0
    }
293
}
294
295
extension NIOFileHandle {
296
    /// `Mode` represents file access modes.
297
    public struct Mode: OptionSet, Sendable {
298
        public let rawValue: UInt8
299
300
        @inlinable
301
0
        public init(rawValue: UInt8) {
302
0
            self.rawValue = rawValue
303
0
        }
304
305
0
        internal var posixFlags: CInt {
306
0
            switch self {
307
0
            case [.read, .write]:
308
0
                return O_RDWR
309
0
            case .read:
310
0
                return O_RDONLY
311
0
            case .write:
312
0
                return O_WRONLY
313
0
            default:
314
0
                preconditionFailure("Unsupported mode value")
315
0
            }
316
0
        }
317
318
        /// Opens file for reading
319
        @inlinable
320
0
        public static var read: Mode { Mode(rawValue: 1 << 0) }
321
        /// Opens file for writing
322
        @inlinable
323
0
        public static var write: NIOFileHandle.Mode { Mode(rawValue: 1 << 1) }
324
    }
325
326
    /// `Flags` allows to specify additional flags to `Mode`, such as permission for file creation.
327
    public struct Flags: Sendable {
328
        @usableFromInline
329
        internal var posixMode: NIOPOSIXFileMode
330
331
        @usableFromInline
332
        internal var posixFlags: CInt
333
334
        @inlinable
335
0
        internal init(posixMode: NIOPOSIXFileMode, posixFlags: CInt) {
336
0
            self.posixMode = posixMode
337
0
            self.posixFlags = posixFlags
338
0
        }
339
340
0
        public static var `default`: Flags { Flags(posixMode: 0, posixFlags: 0) }
341
342
        #if os(Windows)
343
        public static let defaultPermissions = _S_IREAD | _S_IWRITE
344
        #elseif os(WASI)
345
        public static let defaultPermissions = WASILibc.S_IWUSR | WASILibc.S_IRUSR | WASILibc.S_IRGRP | WASILibc.S_IROTH
346
        #else
347
        public static let defaultPermissions = S_IWUSR | S_IRUSR | S_IRGRP | S_IROTH
348
        #endif
349
350
        /// Allows file creation when opening file for writing. File owner is set to the effective user ID of the process.
351
        ///
352
        /// - Parameters:
353
        ///   - posixMode: `file mode` applied when file is created. Default permissions are: read and write for fileowner, read for owners group and others.
354
0
        public static func allowFileCreation(posixMode: NIOPOSIXFileMode = defaultPermissions) -> Flags {
355
0
            #if os(WASI)
356
0
            let flags = CNIOWASI_O_CREAT()
357
0
            #else
358
0
            let flags = O_CREAT
359
0
            #endif
360
0
            return Flags(posixMode: posixMode, posixFlags: flags)
361
0
        }
362
363
        /// Allows the specification of POSIX flags (e.g. `O_TRUNC`) and mode (e.g. `S_IWUSR`)
364
        ///
365
        /// - Parameters:
366
        ///   - flags: The POSIX open flags (the second parameter for `open(2)`).
367
        ///   - mode: The POSIX mode (the third parameter for `open(2)`).
368
        /// - Returns: A `NIOFileHandle.Mode` equivalent to the given POSIX flags and mode.
369
0
        public static func posix(flags: CInt, mode: NIOPOSIXFileMode) -> Flags {
370
0
            Flags(posixMode: mode, posixFlags: flags)
371
0
        }
372
    }
373
374
    /// Open a new `NIOFileHandle`. This operation is blocking.
375
    ///
376
    /// - Parameters:
377
    ///   - path: The path of the file to open. The ownership of the file descriptor is transferred to this `NIOFileHandle` and so it will be closed once `close` is called.
378
    ///   - mode: Access mode. Default mode is `.read`.
379
    ///   - flags: Additional POSIX flags.
380
    @available(
381
        *,
382
        deprecated,
383
        message: """
384
            Avoid using NIOFileHandle. The type is difficult to hold correctly, \
385
            use NIOFileSystem as a replacement API.
386
            """
387
    )
388
    public convenience init(
389
        path: String,
390
        mode: Mode = .read,
391
        flags: Flags = .default
392
0
    ) throws {
393
0
        try self.init(_deprecatedPath: path, mode: mode, flags: flags)
394
0
    }
395
396
    /// Open a new `NIOFileHandle`. This operation is blocking.
397
    ///
398
    /// - Parameters:
399
    ///   - path: The path of the file to open. The ownership of the file descriptor is transferred to this `NIOFileHandle` and so it will be closed once `close` is called.
400
    ///   - mode: Access mode. Default mode is `.read`.
401
    ///   - flags: Additional POSIX flags.
402
    @available(*, noasync, message: "This method may block the calling thread")
403
    public convenience init(
404
        _deprecatedPath path: String,
405
        mode: Mode = .read,
406
        flags: Flags = .default
407
0
    ) throws {
408
0
        #if os(Windows)
409
0
        let fl = mode.posixFlags | flags.posixFlags | _O_NOINHERIT
410
0
        #else
411
0
        let fl = mode.posixFlags | flags.posixFlags | O_CLOEXEC
412
0
        #endif
413
0
        let fd = try SystemCalls.open(file: path, oFlag: fl, mode: flags.posixMode)
414
0
        self.init(_deprecatedTakingOwnershipOfDescriptor: fd)
415
0
    }
416
417
    /// Open a new `NIOFileHandle`. This operation is blocking.
418
    ///
419
    /// - Parameters:
420
    ///   - path: The path of the file to open. The ownership of the file descriptor is transferred to this `NIOFileHandle` and so it will be closed once `close` is called.
421
    @available(
422
        *,
423
        deprecated,
424
        message: """
425
            Avoid using NIOFileHandle. The type is difficult to hold correctly, \
426
            use NIOFileSystem as a replacement API.
427
            """
428
    )
429
0
    public convenience init(path: String) throws {
430
0
        try self.init(_deprecatedPath: path)
431
0
    }
432
433
    /// Open a new `NIOFileHandle`. This operation is blocking.
434
    ///
435
    /// - Parameters:
436
    ///   - path: The path of the file to open. The ownership of the file descriptor is transferred to this `NIOFileHandle` and so it will be closed once `close` is called.
437
    @available(*, noasync, message: "This method may block the calling thread")
438
0
    public convenience init(_deprecatedPath path: String) throws {
439
0
        // This function is here because we had a function like this in NIO 2.0, and the one above doesn't quite match. Sadly we can't
440
0
        // really deprecate this either, because it'll be preferred to the one above in many cases.
441
0
        try self.init(_deprecatedPath: path, mode: .read, flags: .default)
442
0
    }
443
}
444
445
extension NIOFileHandle: CustomStringConvertible {
446
0
    public var description: String {
447
0
        "FileHandle { descriptor: \(FileDescriptorState(rawValue: self.descriptor.load(ordering: .relaxed)).descriptor) }"
448
0
    }
449
}