/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 | | } |