diff --git a/Sources/CSystem/include/CSystemLinux.h b/Sources/CSystem/include/CSystemLinux.h index b172d658..70bd1474 100644 --- a/Sources/CSystem/include/CSystemLinux.h +++ b/Sources/CSystem/include/CSystemLinux.h @@ -11,6 +11,7 @@ #include #include +#include #include #include #include diff --git a/Sources/System/FileControl.swift b/Sources/System/FileControl.swift new file mode 100644 index 00000000..c9397661 --- /dev/null +++ b/Sources/System/FileControl.swift @@ -0,0 +1,292 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2021 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + */ + +#if !os(Windows) + + +// Strongly typed, Swifty interfaces to the most common and useful `fcntl` +// commands. + +extension FileDescriptor { + /// Get the flags associated with this file descriptor + /// + /// The corresponding C function is `fcntl` with the `F_GETFD` command. + @_alwaysEmitIntoClient + public func getFlags(retryOnInterrupt: Bool = true) throws -> Flags { + try Flags(rawValue: control( + .getFlags, retryOnInterrupt: retryOnInterrupt)) + } + + /// Set the file descriptor flags. + /// + /// The corresponding C function is `fcntl` with the `F_SETFD` command. + @_alwaysEmitIntoClient + public func setFlags( + _ value: Flags, retryOnInterrupt: Bool = true + ) throws { + _ = try control( + .setFlags, value.rawValue, retryOnInterrupt: retryOnInterrupt) + } + + /// Get descriptor status flags. + /// + /// The corresponding C function is `fcntl` with the `F_GETFL` command. + @_alwaysEmitIntoClient + public func getStatusFlags( + retryOnInterrupt: Bool = true + ) throws -> StatusFlags { + try StatusFlags(rawValue: control( + .getStatusFlags, retryOnInterrupt: retryOnInterrupt)) + } + + /// Set descriptor status flags. + /// + /// The corresponding C function is `fcntl` with the `F_SETFL` command. + @_alwaysEmitIntoClient + public func setStatusFlags( + _ flags: StatusFlags, retryOnInterrupt: Bool = true + ) throws { + _ = try control( + .setStatusFlags, flags.rawValue, retryOnInterrupt: retryOnInterrupt) + } + + /// Get the process ID or process ID group currently receiving SIGIO and + /// SIGURG signals. + /// + /// The corresponding C function is `fcntl` with the `F_GETOWN` command. + @_alwaysEmitIntoClient + public func getOwner( + retryOnInterrupt: Bool = true + ) throws -> (ProcessID, isGroup: Bool) { + let pidOrPGID = try control( + .getOwner, retryOnInterrupt: retryOnInterrupt) + if pidOrPGID < 0 { + return (ProcessID(rawValue: -pidOrPGID), isGroup: true) + } + return (ProcessID(rawValue: pidOrPGID), isGroup: false) + } + + /// Set the process ID or process group ID to receive SIGIO and SIGURG + /// signals. + /// + /// The corresponding C function is `fcntl` with the `F_SETOWN` command. + @_alwaysEmitIntoClient + public func setOwner( + _ id: ProcessID, isGroup: Bool, retryOnInterrupt: Bool = true + ) throws { + let pidOrPGID = isGroup ? -id.rawValue : id.rawValue + _ = try control(.setOwner, pidOrPGID, retryOnInterrupt: retryOnInterrupt) + } +} + +extension FileDescriptor { + /// Duplicate this file descriptor and return the newly created copy. + /// + /// - Parameters: + /// - `minRawValue`: A lower bound on the new file descriptor's raw value. + /// - `closeOnExec`: Whether the new descriptor's `closeOnExec` flag is set. + /// - Returns: The lowest numbered available descriptor whose raw value is + /// greater than or equal to `minRawValue`. + /// + /// File descriptors are merely references to some underlying system + /// resource. The system does not distinguish between the original and the + /// new file descriptor in any way. For example, read, write and seek + /// operations on one of them also affect the logical file position in the + /// other, and append mode, non-blocking I/O and asynchronous I/O options + /// are shared between the references. If a separate pointer into the file + /// is desired, a different object reference to the file must be obtained + /// by issuing an additional call to `open`. + /// + /// However, each file descriptor maintains its own close-on-exec flag. + /// + /// The corresponding C functions are `fcntl` with `F_DUPFD` and + /// `F_DUPFD_CLOEXEC`. + @_alwaysEmitIntoClient + public func duplicate( + minRawValue: CInt, closeOnExec: Bool, retryOnInterrupt: Bool = true + ) throws -> FileDescriptor { + let cmd: Command = closeOnExec ? .duplicateCloseOnExec : .duplicate + return try FileDescriptor(rawValue: control( + cmd, minRawValue, retryOnInterrupt: retryOnInterrupt)) + } + +#if !os(Linux) + + /// Get the path of the file descriptor + /// + /// - Parameters: + /// - `noFirmLink`: Get the non firmlinked path of the file descriptor. + /// + /// The corresponding C functions are `fcntl` with `F_GETPATH` and + /// `F_GETPATH_NOFIRMLINK`. + public func getPath( + noFirmLink: Bool = false, retryOnInterrupt: Bool = true + ) throws -> FilePath { + let cmd: Command = noFirmLink ? .getPathNoFirmLink : .getPath + // TODO: have a uninitialized init on FilePath / SystemString... + let bytes = try Array(unsafeUninitializedCapacity: _maxPathLen) { + (bufPtr, count: inout Int) in + _ = try control( + cmd, + UnsafeMutableRawPointer(bufPtr.baseAddress!), + retryOnInterrupt: retryOnInterrupt) + // TODO: The below is probably the wrong formulation... + count = system_strlen( + UnsafeRawPointer( + bufPtr.baseAddress! + ).assumingMemoryBound(to: Int8.self)) + } + return FilePath(SystemString(nullTerminated: bytes)) + } + +#endif // !os(Linux) + +} + +extension FileDescriptor { + /// Low-level interface equivalent to C's `fcntl`. Note, most common + /// operations have Swiftier alternatives directly on `FileDescriptor`. + @_alwaysEmitIntoClient + public func control( + _ cmd: Command, retryOnInterrupt: Bool = true + ) throws -> CInt { + try _control(cmd, retryOnInterrupt: retryOnInterrupt).get() + } + + /// Low-level interface equivalent to C's `fcntl`. Note, most common + /// operations have Swiftier alternatives directly on `FileDescriptor`. + @_alwaysEmitIntoClient + public func control( + _ cmd: Command, _ arg: CInt, retryOnInterrupt: Bool = true + ) throws -> CInt { + try _control(cmd, arg, retryOnInterrupt: retryOnInterrupt).get() + } + + /// Low-level interface equivalent to C's `fcntl`. Note, most common + /// operations have Swiftier alternatives directly on `FileDescriptor`. + @_alwaysEmitIntoClient + public func control( + _ cmd: Command, + _ ptr: UnsafeMutableRawPointer, + retryOnInterrupt: Bool = true + ) throws -> CInt { + try _control(cmd, ptr, retryOnInterrupt: retryOnInterrupt).get() + } + + /// Low-level interface equivalent to C's `fcntl`. Note, most common + /// operations have Swiftier alternatives directly on `FileDescriptor`. + @_alwaysEmitIntoClient + public func control( + _ cmd: Command, + _ lock: inout FileDescriptor.FileLock, + retryOnInterrupt: Bool = true + ) throws -> CInt { + try _control(cmd, &lock, retryOnInterrupt: retryOnInterrupt).get() + } + +#if !os(Linux) + + // TODO: Worth calling out the command to supply in docs? + + /// Low-level interface equivalent to C's `fcntl`. Note, most common + /// operations have Swiftier alternatives directly on `FileDescriptor`. + @_alwaysEmitIntoClient + public func control( + _ cmd: Command, + _ type: inout FileDescriptor.ControlTypes.Store, + retryOnInterrupt: Bool = true + ) throws -> CInt { + try withUnsafeMutablePointer(to: &type) { + try _control(cmd, $0, retryOnInterrupt: retryOnInterrupt).get() + } + } + + /// Low-level interface equivalent to C's `fcntl`. Note, most common + /// operations have Swiftier alternatives directly on `FileDescriptor`. + @_alwaysEmitIntoClient + public func control( + _ cmd: Command, + _ type: inout FileDescriptor.ControlTypes.Punchhole, + retryOnInterrupt: Bool = true + ) throws -> CInt { + try withUnsafeMutablePointer(to: &type) { + try _control(cmd, $0, retryOnInterrupt: retryOnInterrupt).get() + } + } + + /// Low-level interface equivalent to C's `fcntl`. Note, most common + /// operations have Swiftier alternatives directly on `FileDescriptor`. + @_alwaysEmitIntoClient + public func control( + _ cmd: Command, + _ type: inout FileDescriptor.ControlTypes.ReadAdvisory, + retryOnInterrupt: Bool = true + ) throws -> CInt { + try withUnsafeMutablePointer(to: &type) { + try _control(cmd, $0, retryOnInterrupt: retryOnInterrupt).get() + } + } + + /// Low-level interface equivalent to C's `fcntl`. Note, most common + /// operations have Swiftier alternatives directly on `FileDescriptor`. + @_alwaysEmitIntoClient + public func control( + _ cmd: Command, + _ type: inout FileDescriptor.ControlTypes.LogicalToPhysical, + retryOnInterrupt: Bool = true + ) throws -> CInt { + try withUnsafeMutablePointer(to: &type) { + try _control(cmd, $0, retryOnInterrupt: retryOnInterrupt).get() + } + } + +#endif // !os(Linux) + + @usableFromInline + internal func _control( + _ cmd: Command, retryOnInterrupt: Bool + ) -> Result { + valueOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_fcntl(self.rawValue, cmd.rawValue) + } + } + @usableFromInline + internal func _control( + _ cmd: Command, _ arg: CInt, retryOnInterrupt: Bool + ) -> Result { + valueOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_fcntl(self.rawValue, cmd.rawValue, arg) + } + } + @usableFromInline + internal func _control( + _ cmd: Command, + _ ptr: UnsafeMutableRawPointer, + retryOnInterrupt: Bool + ) -> Result { + valueOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_fcntl(self.rawValue, cmd.rawValue, ptr) + } + } + + @usableFromInline + internal func _control( + _ cmd: Command, + _ lock: inout FileDescriptor.FileLock, + retryOnInterrupt: Bool + ) -> Result<(), Errno> { + nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + withUnsafeMutablePointer(to: &lock) { + system_fcntl(self.rawValue, cmd.rawValue, $0) + } + } + } +} + +#endif // !os(Windows) diff --git a/Sources/System/FileControlCommand.swift b/Sources/System/FileControlCommand.swift new file mode 100644 index 00000000..71183baf --- /dev/null +++ b/Sources/System/FileControlCommand.swift @@ -0,0 +1,495 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2021 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + */ + +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +import Darwin +#elseif os(Linux) || os(FreeBSD) || os(Android) +import Glibc +import CSystem +#elseif os(Windows) +// Nothing +#else +#error("Unsupported Platform") +#endif + +#if !os(Windows) + +extension FileDescriptor { + /// Commands (and various constants) to pass to `fcntl`. + @frozen + public struct Command: RawRepresentable, Hashable, Codable, Sendable { + @_alwaysEmitIntoClient + public let rawValue: CInt + + @_alwaysEmitIntoClient + public init(rawValue: CInt) { self.rawValue = rawValue } + } +} + +extension FileDescriptor.Command { + @_alwaysEmitIntoClient + private init(_ rawValue: CInt) { self.init(rawValue: rawValue) } + + /// Duplicate file descriptor. + /// + /// Note: A Swiftier wrapper is + /// `FileDescriptor.duplicate(minRawValue:closeOnExec:)`. + /// + /// The corresponding C constant is `F_DUPFD`. + @_alwaysEmitIntoClient + public static var duplicate: Self { .init(F_DUPFD) } + + /// Mark the dup with FD_CLOEXEC. + /// + /// The corresponding C constant is `F_DUPFD_CLOEXEC` + @_alwaysEmitIntoClient + public static var duplicateCloseOnExec: Self { + .init(F_DUPFD_CLOEXEC) + } + + /// Get file descriptor flags. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.getFlags()`. + /// + /// The corresponding C constant is `F_GETFD`. + @_alwaysEmitIntoClient + public static var getFlags: Self { .init(F_GETFD) } + + /// Set file descriptor flags. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.setFlags(_:)`. + /// + /// The corresponding C constant is `F_SETFD`. + @_alwaysEmitIntoClient + public static var setFlags: Self { .init(F_SETFD) } + + /// Get file status flags. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.getStatusFlags()`. + /// + /// The corresponding C constant is `F_GETFL`. + @_alwaysEmitIntoClient + public static var getStatusFlags: Self { + .init(F_GETFL) + } + + /// Set file status flags. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.setStatusFlags(_:)`. + /// + /// The corresponding C constant is `F_SETFL`. + @_alwaysEmitIntoClient + public static var setStatusFlags: Self { + .init(F_SETFL) + } + + /// Get SIGIO/SIGURG proc/pgrp. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.getOwner()`. + /// + /// The corresponding C constant is `F_GETOWN`. + @_alwaysEmitIntoClient + public static var getOwner: Self { .init(F_GETOWN) } + + /// Set SIGIO/SIGURG proc/pgrp. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.setOwner(_:)`. + /// + /// The corresponding C constant is `F_SETOWN`. + @_alwaysEmitIntoClient + public static var setOwner: Self { .init(F_SETOWN) } + + /// Get record locking information. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.getLock(_:_:)`. + /// + /// The corresponding C constant is `F_GETLK`. + @_alwaysEmitIntoClient + public static var getLock: Self { .init(F_GETLK) } + + /// Set record locking information. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.setLock(_:_:)`. + /// + /// The corresponding C constant is `F_SETLK`. + @_alwaysEmitIntoClient + public static var setLock: Self { .init(F_SETLK) } + + /// Wait if blocked. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.setLock(_:_:)`. + /// + /// The corresponding C constant is `F_SETLKW`. + @_alwaysEmitIntoClient + public static var setLockWait: Self { .init(F_SETLKW) } + +#if !os(Linux) + + /// Wait if blocked, return on timeout. + /// + /// TODO: A Swiftier wrapper + /// + /// The corresponding C constant is `F_SETLKWTIMEOUT`. + @_alwaysEmitIntoClient + public static var setLockWaitTimout: Self { + .init(F_SETLKWTIMEOUT) + } + + /// The corresponding C constant is `F_FLUSH_DATA`. + @_alwaysEmitIntoClient + public static var flushData: Self { + .init(F_FLUSH_DATA) + } + + /// Used for regression test. + /// + /// The corresponding C constant is `F_CHKCLEAN`. + @_alwaysEmitIntoClient + public static var checkClean: Self { .init(F_CHKCLEAN) } + + // TODO: Higher level API which will call `fallocate(2)` on Linux and use + // TODO: the below on Darwin. Then we can call out to that. + + /// Preallocate storage. + /// + /// The corresponding C constant is `F_PREALLOCATE`. + @_alwaysEmitIntoClient + public static var preallocate: Self { + .init(F_PREALLOCATE) + } + + /// Truncate a file. Equivalent to calling truncate(2). + /// + /// The corresponding C constant is `F_SETSIZE`. + @_alwaysEmitIntoClient + public static var setSize: Self { .init(F_SETSIZE) } + + /// Issue an advisory read async with no copy to user. + /// + /// The corresponding C constant is `F_RDADVISE`. + @_alwaysEmitIntoClient + public static var readAdvise: Self { .init(F_RDADVISE) } + + /// Turn read ahead off/on for this fd. + /// + /// The corresponding C constant is `F_RDAHEAD`. + @_alwaysEmitIntoClient + public static var readAhead: Self { .init(F_RDAHEAD) } + + /// Turn data caching off/on for this fd. + /// + /// The corresponding C constant is `F_NOCACHE`. + @_alwaysEmitIntoClient + public static var noCache: Self { .init(F_NOCACHE) } + + /// File offset to device offset. + /// + /// The corresponding C constant is `F_LOG2PHYS`. + @_alwaysEmitIntoClient + public static var logicalToPhysical: Self { .init(F_LOG2PHYS) } + + /// Return the full path of the fd. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.getPath(_:)`. + /// + /// The corresponding C constant is `F_GETPATH`. + @_alwaysEmitIntoClient + public static var getPath: Self { .init(F_GETPATH) } + + /// Synchronize the file system and ask the drive to flush to the media. + /// + /// The corresponding C constant is `F_FULLFSYNC`. + @_alwaysEmitIntoClient + public static var fullFileSystemSync: Self { .init(F_FULLFSYNC) } + + /// Find which component (if any) is a package. + /// + /// The corresponding C constant is `F_PATHPKG_CHECK`. + @_alwaysEmitIntoClient + public static var pathPackageCheck: Self { + .init(F_PATHPKG_CHECK) + } + + /// "Freeze" all file system operations. + /// + /// The corresponding C constant is `F_FREEZE_FS`. + @_alwaysEmitIntoClient + public static var freezeFileSystem: Self { .init(F_FREEZE_FS) } + + /// "Thaw" all file system operations. + /// + /// The corresponding C constant is `F_THAW_FS`. + @_alwaysEmitIntoClient + public static var thawFileSystem: Self { .init(F_THAW_FS) } + + /// Turn data caching off/on (globally) for this file. + /// + /// The corresponding C constant is `F_GLOBAL_NOCACHE`. + @_alwaysEmitIntoClient + public static var globalNoCache: Self { + .init(F_GLOBAL_NOCACHE) + } + + /// Add detached signatures. + /// + /// The corresponding C constant is `F_ADDSIGS`. + @_alwaysEmitIntoClient + public static var addSignatures: Self { + .init(F_ADDSIGS) + } + + /// Add signature from same file (used by dyld for shared libs). + /// + /// The corresponding C constant is `F_ADDFILESIGS`. + @_alwaysEmitIntoClient + public static var addFileSignatures: Self { + .init(F_ADDFILESIGS) + } + + /// Used in conjunction with `.noCache` to indicate that DIRECT, + /// synchonous writes should not be used (i.e. its ok to temporaily create + /// cached pages). + /// + /// The corresponding C constant is `F_NODIRECT`. + @_alwaysEmitIntoClient + public static var noDirect: Self { .init(F_NODIRECT) } + + /// Get the protection class of a file from the EA, returns int. + /// + /// The corresponding C constant is `F_GETPROTECTIONCLASS`. + @_alwaysEmitIntoClient + public static var getProtectionClass: Self { + .init(F_GETPROTECTIONCLASS) + } + + /// Set the protection class of a file for the EA, requires int. + /// + /// The corresponding C constant is `F_SETPROTECTIONCLASS`. + @_alwaysEmitIntoClient + public static var setProtectionClass: Self { + .init(F_SETPROTECTIONCLASS) + } + + /// File offset to device offset, extended. + /// + /// The corresponding C constant is `F_LOG2PHYS_EXT`. + @_alwaysEmitIntoClient + public static var logicalToPhysicalExtended: Self { + .init(F_LOG2PHYS_EXT) + } + + /// Get record locking information, per-process. + /// + /// The corresponding C constant is `F_GETLKPID`. + @_alwaysEmitIntoClient + public static var getLockPID: Self { .init(F_GETLKPID) } + + /// Mark the file as being the backing store for another filesystem. + /// + /// The corresponding C constant is `F_SETBACKINGSTORE`. + @_alwaysEmitIntoClient + public static var setBackingStore: Self { + .init(F_SETBACKINGSTORE) + } + + /// Return the full path of the FD, but error in specific mtmd + /// circumstances. + /// + /// The corresponding C constant is `F_GETPATH_MTMINFO`. + @_alwaysEmitIntoClient + public static var getPathMTMDInfo: Self { + .init(F_GETPATH_MTMINFO) + } + + /// Returns the code directory, with associated hashes, to the caller. + /// + /// The corresponding C constant is `F_GETCODEDIR`. + @_alwaysEmitIntoClient + public static var getCodeDirectory: Self { + .init(F_GETCODEDIR) + } + + /// No SIGPIPE generated on EPIPE. + /// + /// The corresponding C constant is `F_SETNOSIGPIPE`. + @_alwaysEmitIntoClient + public static var setNoSigPipe: Self { + .init(F_SETNOSIGPIPE) + } + + /// Status of SIGPIPE for this fd. + /// + /// The corresponding C constant is `F_GETNOSIGPIPE`. + @_alwaysEmitIntoClient + public static var getNoSigPipe: Self { + .init(F_GETNOSIGPIPE) + } + + /// For some cases, we need to rewrap the key for AKS/MKB. + /// + /// The corresponding C constant is `F_TRANSCODEKEY`. + @_alwaysEmitIntoClient + public static var transcodeKey: Self { + .init(F_TRANSCODEKEY) + } + + /// File being written to a by single writer... if throttling enabled, + /// writes may be broken into smaller chunks with throttling in between. + /// + /// The corresponding C constant is `F_SINGLE_WRITER`. + @_alwaysEmitIntoClient + public static var singleWriter: Self { + .init(F_SINGLE_WRITER) + } + + + /// Get the protection version number for this filesystem. + /// + /// The corresponding C constant is `F_GETPROTECTIONLEVEL`. + @_alwaysEmitIntoClient + public static var getProtectionLevel: Self { + .init(F_GETPROTECTIONLEVEL) + } + + + /// Add detached code signatures (used by dyld for shared libs). + /// + /// The corresponding C constant is `F_FINDSIGS`. + @_alwaysEmitIntoClient + public static var findSignatures: Self { + .init(F_FINDSIGS) + } + + /// Add signature from same file, only if it is signed by Apple (used by + /// dyld for simulator). + /// + /// The corresponding C constant is `F_ADDFILESIGS_FOR_DYLD_SIM`. + @_alwaysEmitIntoClient + public static var addFileSignaturesForDYLDSim: Self { + .init(F_ADDFILESIGS_FOR_DYLD_SIM) + } + + /// Fsync + issue barrier to drive. + /// + /// The corresponding C constant is `F_BARRIERFSYNC`. + @_alwaysEmitIntoClient + public static var barrierFileSystemSync: Self { + .init(F_BARRIERFSYNC) + } + + /// Add signature from same file, return end offset in structure on success. + /// + /// The corresponding C constant is `F_ADDFILESIGS_RETURN`. + @_alwaysEmitIntoClient + public static var addFileSignaturesReturn: Self { + .init(F_ADDFILESIGS_RETURN) + } + + /// Check if Library Validation allows this Mach-O file to be mapped into + /// the calling process. + /// + /// The corresponding C constant is `F_CHECK_LV`. + @_alwaysEmitIntoClient + public static var checkLibraryValidation: Self { + .init(F_CHECK_LV) + } + + /// Deallocate a range of the file. + /// + /// The corresponding C constant is `F_PUNCHHOLE`. + @_alwaysEmitIntoClient + public static var punchhole: Self { .init(F_PUNCHHOLE) } + + /// Trim an active file. + /// + /// The corresponding C constant is `F_TRIM_ACTIVE_FILE`. + @_alwaysEmitIntoClient + public static var trimActiveFile: Self { + .init(F_TRIM_ACTIVE_FILE) + } + + /// Synchronous advisory read fcntl for regular and compressed file. + /// + /// The corresponding C constant is `F_SPECULATIVE_READ`. + @_alwaysEmitIntoClient + public static var speculativeRead: Self { + .init(F_SPECULATIVE_READ) + } + + /// Return the full path without firmlinks of the fd. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.getPath(_:)`. + /// + /// The corresponding C constant is `F_GETPATH_NOFIRMLINK`. + @_alwaysEmitIntoClient + public static var getPathNoFirmLink: Self { + .init(F_GETPATH_NOFIRMLINK) + } + + /// Add signature from same file, return information. + /// + /// The corresponding C constant is `F_ADDFILESIGS_INFO`. + @_alwaysEmitIntoClient + public static var addFileSignatureInfo: Self { + .init(F_ADDFILESIGS_INFO) + } + + /// Add supplemental signature from same file with fd reference to original. + /// + /// The corresponding C constant is `F_ADDFILESUPPL`. + @_alwaysEmitIntoClient + public static var addFileSupplementalSignature: Self { + .init(F_ADDFILESUPPL) + } + + /// Look up code signature information attached to a file or slice. + /// + /// The corresponding C constant is `F_GETSIGSINFO`. + @_alwaysEmitIntoClient + public static var getSignatureInfo: Self { + .init(F_GETSIGSINFO) + } + +#endif // !os(Linux) + + /// Get open file description record locking information. + /// + /// The corresponding C constant is `F_GETLK`. + @_alwaysEmitIntoClient + public static var getOFDLock: Self { .init(_F_OFD_GETLK) } + + /// Set open file description record locking information. + /// + /// The corresponding C constant is `F_SETLK`. + @_alwaysEmitIntoClient + public static var setOFDLock: Self { .init(_F_OFD_SETLK) } + + /// Set open file description record locking information and wait until + /// the request can be completed. + /// + /// The corresponding C constant is `F_SETLKW`. + @_alwaysEmitIntoClient + public static var setOFDLockWait: Self { .init(_F_OFD_SETLKW) } + +#if !os(Linux) + /// Set open file description record locking information and wait until + /// the request can be completed, returning on timeout. + /// + /// The corresponding C constant is `F_SETLKWTIMEOUT`. + @_alwaysEmitIntoClient + public static var setOFDLockWaitTimout: Self { + .init(_F_OFD_SETLKWTIMEOUT) + } +#endif +} + +internal var _maxPathLen: Int { Int(MAXPATHLEN) } + +#endif // !os(Windows) + diff --git a/Sources/System/FileControlRaw.swift b/Sources/System/FileControlRaw.swift new file mode 100644 index 00000000..e69de29b diff --git a/Sources/System/FileControlTypes.swift b/Sources/System/FileControlTypes.swift new file mode 100644 index 00000000..7f98165e --- /dev/null +++ b/Sources/System/FileControlTypes.swift @@ -0,0 +1,336 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2021 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + */ + +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +import Darwin +#elseif os(Linux) || os(FreeBSD) || os(Android) +import Glibc +import CSystem +#elseif os(Windows) +// Nothing +#else +#error("Unsupported Platform") +#endif + +#if !os(Windows) + +// MARK: - RawRepresentable wrappers +extension FileDescriptor { + /// File descriptor flags. + @frozen + public struct Flags: OptionSet, Sendable { + @_alwaysEmitIntoClient + public var rawValue: CInt + + @_alwaysEmitIntoClient + public init(rawValue: CInt) { self.rawValue = rawValue } + + /// The given file descriptor will be automatically closed in the + /// successor process image when one of the execv(2) or posix_spawn(2) + /// family of system calls is invoked. + /// + /// The corresponding C global is `FD_CLOEXEC`. + @_alwaysEmitIntoClient + public static var closeOnExec: Flags { Flags(rawValue: FD_CLOEXEC) } + } + + /// File descriptor status flags. + @frozen + public struct StatusFlags: OptionSet, Sendable { + @_alwaysEmitIntoClient + public var rawValue: CInt + + @_alwaysEmitIntoClient + public init(rawValue: CInt) { self.rawValue = rawValue } + + @_alwaysEmitIntoClient + fileprivate init(_ raw: CInt) { self.init(rawValue: raw) } + + /// Non-blocking I/O; if no data is available to a read + /// call, or if a write operation would block, the read or + /// write call throws `Errno.resourceTemporarilyUnavailable`. + /// + /// The corresponding C constant is `O_NONBLOCK`. + @_alwaysEmitIntoClient + public static var nonBlocking: StatusFlags { StatusFlags(O_NONBLOCK) } + + /// Force each write to append at the end of file; corre- + /// sponds to `OpenOptions.append`. + /// + /// The corresponding C constant is `O_APPEND`. + @_alwaysEmitIntoClient + public static var append: StatusFlags { StatusFlags(O_APPEND) } + + /// Enable the SIGIO signal to be sent to the process + /// group when I/O is possible, e.g., upon availability of + /// data to be read. + /// + /// The corresponding C constant is `O_ASYNC`. + @_alwaysEmitIntoClient + public static var async: StatusFlags { StatusFlags(O_ASYNC) } + } +} + +#if !os(Linux) +extension FileDescriptor { + /// Namespace for types used with `FileDescriptor.control`. + @frozen + public enum ControlTypes { } +} + +extension FileDescriptor.ControlTypes { + /// The corresponding C type is `fstore`. + @frozen + public struct Store: RawRepresentable, Sendable { + @_alwaysEmitIntoClient + public var rawValue: CInterop.FStore + + @_alwaysEmitIntoClient + public init(rawValue: CInterop.FStore) { self.rawValue = rawValue } + + @frozen + public struct Flags: OptionSet, Sendable { + @_alwaysEmitIntoClient + public var rawValue: UInt32 + + @_alwaysEmitIntoClient + public init(rawValue: UInt32) { self.rawValue = rawValue } + } + + @frozen + public struct PositionMode: RawRepresentable, Hashable, Sendable { + @_alwaysEmitIntoClient + public var rawValue: CInt + + @_alwaysEmitIntoClient + public init(rawValue: CInt) { self.rawValue = rawValue } + } + } + + /// The corresponding C type is `fpunchhole` + @frozen + public struct Punchhole: RawRepresentable, Sendable { + @_alwaysEmitIntoClient + public var rawValue: CInterop.FPunchhole + + @_alwaysEmitIntoClient + public init(rawValue: CInterop.FPunchhole) { self.rawValue = rawValue } + + @frozen + public struct Flags: OptionSet, Sendable { + @_alwaysEmitIntoClient + public var rawValue: UInt32 + + @_alwaysEmitIntoClient + public init(rawValue: UInt32) { self.rawValue = rawValue } + } + } + + /// The corresponding C type is `radvisory` + @frozen + public struct ReadAdvisory: RawRepresentable, Sendable { + @_alwaysEmitIntoClient + public var rawValue: CInterop.RAdvisory + + @_alwaysEmitIntoClient + public init(rawValue: CInterop.RAdvisory) { self.rawValue = rawValue } + } + + /// The corresponding C type is `log2phys` + @frozen + public struct LogicalToPhysical: RawRepresentable, Sendable { + @_alwaysEmitIntoClient + public var rawValue: CInterop.Log2Phys + + @_alwaysEmitIntoClient + public init(rawValue: CInterop.Log2Phys) { self.rawValue = rawValue } + + @frozen + public struct Flags: OptionSet, Sendable { + @_alwaysEmitIntoClient + public let rawValue: UInt32 + + @_alwaysEmitIntoClient + public init(rawValue: UInt32) { self.rawValue = rawValue } + } + } +} + +extension FileDescriptor.ControlTypes.Store.Flags { + @_alwaysEmitIntoClient + private init(_ rawSignedValue: Int32) { + self.init(rawValue: UInt32(truncatingIfNeeded: rawSignedValue)) + } + + /// Allocate contiguous space. (Note that the file system may + /// ignore this request if `length` is very large.) + /// + /// The corresponding C constant is `F_ALLOCATECONTIG` + @_alwaysEmitIntoClient + public var allocateContiguous: Self { .init(F_ALLOCATECONTIG) } + + /// Allocate all requested space or no space at all. + /// + /// The corresponding C constant is `F_ALLOCATEALL` + @_alwaysEmitIntoClient + public var allocateAllOrNone: Self { .init(F_ALLOCATEALL) } + + /// Allocate space that is not freed when close(2) is called. + /// (Note that the file system may ignore this request.) + /// + /// The corresponding C constant is `F_ALLOCATEPERSIST` + @_alwaysEmitIntoClient + public var allocatePersist: Self { .init(F_ALLOCATEPERSIST) } +} + +extension FileDescriptor.ControlTypes.Store.PositionMode { + /// Allocate from the physical end of file. In this case, `length` + /// indicates the number of newly allocated bytes desired. + /// + /// The corresponding C constant is `F_PEOFPOSMODE` + @_alwaysEmitIntoClient + public var physicalEndOfFile: Self { .init(rawValue: F_PEOFPOSMODE) } + + /// Allocate from the volume offset. + /// + /// The corresponding C constant is `F_VOLPOSMODE` + @_alwaysEmitIntoClient + public var volumeOffset: Self { .init(rawValue: F_VOLPOSMODE) } +} + +extension FileDescriptor.ControlTypes.Store { + /// The corresponding C field is `fst_flags` + @_alwaysEmitIntoClient + public var flags: Flags { + get { .init(rawValue: rawValue.fst_flags) } + set { rawValue.fst_flags = newValue.rawValue } + } + + /// Indicates mode for offset field + /// + /// The corresponding C field is `fst_posmode` + @_alwaysEmitIntoClient + public var positionMode: PositionMode { + get { .init(rawValue: rawValue.fst_posmode) } + set { rawValue.fst_posmode = newValue.rawValue } + } + + /// Start of the region + /// + /// The corresponding C field is `fst_offset` + @_alwaysEmitIntoClient + public var offset: Int64 { + get { .init(rawValue.fst_offset) } + set { rawValue.fst_offset = CInterop.Offset(newValue) } + } + + /// Size of the region + /// + /// The corresponding C field is `fst_length` + @_alwaysEmitIntoClient + public var length: Int64 { + get { .init(rawValue.fst_length) } + set { rawValue.fst_length = CInterop.Offset(newValue) } + } + + /// Output: number of bytes allocated + /// + /// The corresponding C field is `fst_bytesalloc` + @_alwaysEmitIntoClient + public var bytesAllocated: Int64 { + get { .init(rawValue.fst_bytesalloc) } + set { rawValue.fst_bytesalloc = CInterop.Offset(newValue) } + } +} + +extension FileDescriptor.ControlTypes.Punchhole { + /// The corresponding C field is `fp_flags` + @_alwaysEmitIntoClient + public var flags: Flags { + get { .init(rawValue: rawValue.fp_flags) } + set { rawValue.fp_flags = newValue.rawValue } + } + + // No API for the reserved field + + /// Start of the region + /// + /// The corresponding C field is `fp_offset` + @_alwaysEmitIntoClient + public var offset: Int64 { + get { .init(rawValue.fp_offset) } + set { rawValue.fp_offset = CInterop.Offset(newValue) } + } + + /// Size of the region + /// + /// The corresponding C field is `fp_length` + @_alwaysEmitIntoClient + public var length: Int64 { + get { .init(rawValue.fp_length) } + set { rawValue.fp_length = CInterop.Offset(newValue) } + } +} + +extension FileDescriptor.ControlTypes.ReadAdvisory { + /// Offset into the file + /// + /// The corresponding C field is `ra_offset` + @_alwaysEmitIntoClient + public var offset: Int64 { + get { .init(rawValue.ra_offset) } + set { rawValue.ra_offset = CInterop.Offset(newValue) } + } + + /// Size of the read + /// + /// The corresponding C field is `ra_count` + @_alwaysEmitIntoClient + public var count: Int { + get { .init(rawValue.ra_count) } + set { rawValue.ra_count = CInt(newValue) } + } +} + +extension FileDescriptor.ControlTypes.LogicalToPhysical { + /// The corresponding C field is `l2p_flags` + @_alwaysEmitIntoClient + public var flags: Flags { + get { .init(rawValue: rawValue.l2p_flags) } + set { rawValue.l2p_flags = newValue.rawValue } + } + + /// When used with `logicalToPhysicalExtended`: + /// - In: number of bytes to be queried; + /// - Out: number of contiguous bytes allocated at this position */ + /// + /// The corresponding C field is `l2p_contigbytes` + @_alwaysEmitIntoClient + public var contiguousBytes: Int64 { + get { .init(rawValue.l2p_contigbytes) } + set { rawValue.l2p_contigbytes = CInterop.Offset(newValue) } + } + + /// When used with `logicalToPhysical`, bytes into file. + /// + /// When used with `logicalToPhysicalExtended`: + /// - In: bytes into file + /// - Out: bytes into device + /// + /// The corresponding C field is `l2p_devoffset` + @_alwaysEmitIntoClient + public var deviceOffset: Int64 { + get { .init(rawValue.l2p_devoffset) } + set { rawValue.l2p_devoffset = CInterop.Offset(newValue) } + } +} + +#endif // !os(Linux) + +#endif // !os(Windows) diff --git a/Sources/System/FileLock.swift b/Sources/System/FileLock.swift new file mode 100644 index 00000000..9d9241bf --- /dev/null +++ b/Sources/System/FileLock.swift @@ -0,0 +1,387 @@ +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +import Darwin +#elseif os(Linux) || os(FreeBSD) || os(Android) +import Glibc +#elseif os(Windows) +// Nothing +#else +#error("Unsupported Platform") +#endif + +#if !os(Windows) +extension FileDescriptor { + /// Advisory record locks. + /// + /// The corresponding C type is `struct flock`. + @frozen + public struct FileLock: RawRepresentable, Sendable { + @_alwaysEmitIntoClient + public var rawValue: CInterop.FileLock + + @_alwaysEmitIntoClient + public init(rawValue: CInterop.FileLock) { self.rawValue = rawValue } + } +} + +extension FileDescriptor.FileLock { + @_alwaysEmitIntoClient + public init() { self.init(rawValue: .init()) } + + /// The type of the locking operation. + /// + /// The corresponding C field is `l_type`. + @_alwaysEmitIntoClient + public var type: Kind { + get { Kind(rawValue: rawValue.l_type) } + set { rawValue.l_type = newValue.rawValue } + } + + /// The origin of the locked region. + /// + /// The corresponding C field is `l_whence`. + @_alwaysEmitIntoClient + public var origin: FileDescriptor.SeekOrigin { + get { FileDescriptor.SeekOrigin(rawValue: CInt(rawValue.l_whence)) } + set { rawValue.l_whence = Int16(newValue.rawValue) } + } + + /// The start offset (from the origin) of the locked region. + /// + /// The corresponding C field is `l_start`. + @_alwaysEmitIntoClient + public var start: Int64 { + get { Int64(rawValue.l_start) } + set { rawValue.l_start = CInterop.Offset(newValue) } + } + + /// The number of consecutive bytes to lock. + /// + /// The corresponding C field is `l_len`. + @_alwaysEmitIntoClient + public var length: Int64 { + get { Int64(rawValue.l_len) } + set { rawValue.l_len = CInterop.Offset(newValue) } + } + + /// The process ID of the lock holder (if applicable). + /// + /// The corresponding C field is `l_pid` + @_alwaysEmitIntoClient + public var pid: ProcessID { + get { ProcessID(rawValue: rawValue.l_pid) } + set { rawValue.l_pid = newValue.rawValue } + } +} + +// MARK: - Convenience for `struct flock` +extension FileDescriptor.FileLock { + // For OFD locks + internal init( + ofdType: Kind, + start: Int64, + length: Int64 + ) { + self.init() + self.type = ofdType + self.start = start + self.length = length + self.pid = ProcessID(rawValue: 0) + } +} + +extension FileDescriptor.FileLock { + /// The kind or type of a lock: read (aka "shared"), write (aka "exclusive"), or none + /// (aka "unlock"). + @frozen + public struct Kind: RawRepresentable, Hashable, Sendable { + @_alwaysEmitIntoClient + public var rawValue: CInterop.CShort + + @_alwaysEmitIntoClient + public init(rawValue: CInterop.CShort) { self.rawValue = rawValue } + + /// Read lock (aka "shared") + /// + /// The corresponding C constant is `F_RDLCK`. + @_alwaysEmitIntoClient + public static var read: Self { + Self(rawValue: CInterop.CShort(truncatingIfNeeded: F_RDLCK)) + } + + /// Write lock (aka "exclusive") + /// + /// The corresponding C constant is `F_WRLCK`. + @_alwaysEmitIntoClient + public static var write: Self { + Self(rawValue: CInterop.CShort(truncatingIfNeeded: F_WRLCK)) + } + + /// No lock (aka "unlock"). + /// + /// The corresponding C constant is `F_UNLCK`. + @_alwaysEmitIntoClient + public static var none: Self { + Self(rawValue: CInterop.CShort(truncatingIfNeeded: F_UNLCK)) + } + + /// Shared (alias for `read`) + @_alwaysEmitIntoClient + public static var shared: Self { .read } + + /// Exclusive (alias for `write`) + @_alwaysEmitIntoClient + public static var exclusive: Self { .write } + + /// Unlock (alias for `none`) + @_alwaysEmitIntoClient + public static var unlock: Self { .none } + } +} + +extension FileDescriptor { + /// Set an advisory open file description lock. + /// + /// If the open file description already has a lock over `byteRange`, that + /// portion of the old lock is replaced. If `byteRange` is `nil`, the + /// entire file is considered. If the lock cannot be set because it is + /// blocked by an existing lock, that is if the syscall would throw + /// `.resourceTemporarilyUnavailable`(aka `EAGAIN`), this will return + /// `false`. + /// + /// Open file description locks are associated with an open file + /// description (see `FileDescriptor.open`). Duplicated + /// file descriptors (see `FileDescriptor.duplicate`) share open file + /// description locks. + /// + /// Locks are advisory, which allow cooperating code to perform + /// consistent operations on files, but do not guarantee consistency. + /// (i.e. other code may still access files without using advisory locks + /// possibly resulting in inconsistencies). + /// + /// Open file description locks are inherited by child processes across + /// `fork`, etc. + /// + /// Passing a lock kind of `.none` will remove a lock (equivalent to calling + /// `FileDescriptor.unlock()`). + /// + /// - Parameters: + /// - kind: The kind of lock to set + /// - byteRange: The range of bytes over which to lock. Pass + /// `nil` to consider the entire file. + /// - retryOnInterrupt: Whether to retry the operation if it throws + /// ``Errno/interrupted``. The default is `true`. Pass `false` to try + /// only once and throw an error upon interruption. + /// - Returns: `true` if the lock was aquired, `false` otherwise + /// + /// The corresponding C function is `fcntl` with `F_OFD_SETLK`. + @_alwaysEmitIntoClient + public func lock( + _ kind: FileDescriptor.FileLock.Kind = .read, + byteRange: (some RangeExpression)? = Range?.none, + retryOnInterrupt: Bool = true + ) throws -> Bool { + let (start, len) = _mapByteRangeToByteOffsets(byteRange) + return try _lock( + kind, + start: start, + length: len, + wait: false, + waitUntilTimeout: false, + retryOnInterrupt: retryOnInterrupt + )?.get() != nil + } + + /// Set an advisory open file description lock. + /// + /// If the open file description already has a lock over `byteRange`, that + /// portion of the old lock is replaced. If `byteRange` is `nil`, the + /// entire file is considered. If the lock cannot be set because it is + /// blocked by an existing lock and `wait` is true, this will wait until + /// the lock can be set, otherwise returns `false`. + /// + /// Open file description locks are associated with an open file + /// description (see `FileDescriptor.open`). Duplicated + /// file descriptors (see `FileDescriptor.duplicate`) share open file + /// description locks. + /// + /// Locks are advisory, which allow cooperating code to perform + /// consistent operations on files, but do not guarantee consistency. + /// (i.e. other code may still access files without using advisory locks + /// possibly resulting in inconsistencies). + /// + /// Open file description locks are inherited by child processes across + /// `fork`, etc. + /// + /// Passing a lock kind of `.none` will remove a lock (equivalent to calling + /// `FileDescriptor.unlock()`). + /// + /// - Parameters: + /// - kind: The kind of lock to set + /// - byteRange: The range of bytes over which to lock. Pass + /// `nil` to consider the entire file. + /// - wait: if `true` will wait until the lock can be set + /// - retryOnInterrupt: Whether to retry the operation if it throws + /// ``Errno/interrupted``. The default is `true`. Pass `false` to try + /// only once and throw an error upon interruption. + /// + /// The corresponding C function is `fcntl` with `F_OFD_SETLK` or `F_OFD_SETLKW`. + @discardableResult + @_alwaysEmitIntoClient + public func lock( + _ kind: FileDescriptor.FileLock.Kind = .read, + byteRange: (some RangeExpression)? = Range?.none, + wait: Bool, + retryOnInterrupt: Bool = true + ) throws -> Bool { + let (start, len) = _mapByteRangeToByteOffsets(byteRange) + return try _lock( + kind, + start: start, + length: len, + wait: wait, + waitUntilTimeout: false, + retryOnInterrupt: retryOnInterrupt + )?.get() != nil + } + +#if !os(Linux) + /// Set an advisory open file description lock. + /// + /// If the open file description already has a lock over `byteRange`, that + /// portion of the old lock is replaced. If `byteRange` is `nil`, the + /// entire file is considered. If the lock cannot be set because it is + /// blocked by an existing lock and `waitUntilTimeout` is true, this will + /// wait until the lock can be set(or the operating system's timeout + /// expires), otherwise returns `false`. + /// + /// Open file description locks are associated with an open file + /// description (see `FileDescriptor.open`). Duplicated + /// file descriptors (see `FileDescriptor.duplicate`) share open file + /// description locks. + /// + /// Locks are advisory, which allow cooperating code to perform + /// consistent operations on files, but do not guarantee consistency. + /// (i.e. other code may still access files without using advisory locks + /// possibly resulting in inconsistencies). + /// + /// Open file description locks are inherited by child processes across + /// `fork`, etc. + /// + /// Passing a lock kind of `.none` will remove a lock (equivalent to calling + /// `FileDescriptor.unlock()`). + /// + /// - Parameters: + /// - kind: The kind of lock to set + /// - byteRange: The range of bytes over which to lock. Pass + /// `nil` to consider the entire file. + /// - waitUntilTimeout: if `true` will wait until the lock can be set or a timeout expires + /// - retryOnInterrupt: Whether to retry the operation if it throws + /// ``Errno/interrupted``. The default is `true`. Pass `false` to try + /// only once and throw an error upon interruption. + /// + /// The corresponding C function is `fcntl` with `F_OFD_SETLK` or `F_SETLKWTIMEOUT`. + @_alwaysEmitIntoClient + public func lock( + _ kind: FileDescriptor.FileLock.Kind = .read, + byteRange: (some RangeExpression)? = Range?.none, + waitUntilTimeout: Bool, + retryOnInterrupt: Bool = true + ) throws -> Bool { + let (start, len) = _mapByteRangeToByteOffsets(byteRange) + return try _lock( + kind, + start: start, + length: len, + wait: false, + waitUntilTimeout: waitUntilTimeout, + retryOnInterrupt: retryOnInterrupt + )?.get() != nil + } +#endif + + /// Remove an open file description lock. + /// + /// Open file description locks are associated with an open file + /// description (see `FileDescriptor.open`). Duplicated + /// file descriptors (see `FileDescriptor.duplicate`) share open file + /// description locks. + /// + /// Locks are advisory, which allow cooperating code to perform + /// consistent operations on files, but do not guarantee consistency. + /// (i.e. other code may still access files without using advisory locks + /// possibly resulting in inconsistencies). + /// + /// Open file description locks are inherited by child processes across + /// `fork`, etc. + /// + /// Calling `unlock()` is equivalent to passing `.none` as the lock kind to + /// `FileDescriptor.lock()`. + /// + /// - Parameters: + /// - byteRange: The range of bytes over which to lock. Pass + /// `nil` to consider the entire file. + /// - retryOnInterrupt: Whether to retry the operation if it throws + /// ``Errno/interrupted``. The default is `true`. Pass `false` to try + /// only once and throw an error upon interruption. + /// + /// The corresponding C function is `fcntl` with `F_OFD_SETLK` or + /// `F_OFD_SETLKW` and a lock type of `F_UNLCK`. + @_alwaysEmitIntoClient + public func unlock( + byteRange: (some RangeExpression)? = Range?.none, + retryOnInterrupt: Bool = true + ) throws { + let (start, len) = _mapByteRangeToByteOffsets(byteRange) + guard try _lock( + .none, + start: start, + length: len, + wait: false, + waitUntilTimeout: false, + retryOnInterrupt: retryOnInterrupt + )?.get() != nil else { + // NOTE: Errno and syscall composition wasn't designed for the modern + // world. Releasing locks should always succeed and never be blocked + // by an existing lock held elsewhere. But there's always a chance + // that some effect (e.g. from NFS) causes `EGAIN` to be thrown for a + // different reason/purpose. Here, in the very unlikely situation + // that we somehow saw it, we convert the `nil` back to the error. + throw Errno.resourceTemporarilyUnavailable + } + } + + /// Internal lock entry point, returns `nil` if blocked by existing lock. + /// Both `wait` and `waitUntilTimeout` cannot both be true (passed as bools to avoid + /// spurious enum in the ABI). + @usableFromInline + internal func _lock( + _ kind: FileDescriptor.FileLock.Kind, + start: Int64, + length: Int64, + wait: Bool, + waitUntilTimeout: Bool, + retryOnInterrupt: Bool + ) -> Result<(), Errno>? { + precondition(!wait || !waitUntilTimeout) + let cmd: FileDescriptor.Command + if waitUntilTimeout { +#if os(Linux) + preconditionFailure("`waitUntilTimeout` unavailable on Linux") + cmd = .setOFDLock +#else + cmd = .setOFDLockWaitTimout +#endif + } else if wait { + cmd = .setOFDLockWait + } else { + cmd = .setOFDLock + } + var lock = FileDescriptor.FileLock( + ofdType: kind, start: start, length: length) + return _extractWouldBlock( + _control(cmd, &lock, retryOnInterrupt: retryOnInterrupt)) + } +} + +#endif // !os(Windows) + diff --git a/Sources/System/Internals/CInterop.swift b/Sources/System/Internals/CInterop.swift index 5e46bafe..7fb416dd 100644 --- a/Sources/System/Internals/CInterop.swift +++ b/Sources/System/Internals/CInterop.swift @@ -45,29 +45,65 @@ public enum CInterop { /// The C `char` type public typealias Char = CChar - #if os(Windows) + /// The C `short` type + public typealias CShort = Int16 + +#if os(Windows) /// The platform's preferred character type. On Unix, this is an 8-bit C /// `char` (which may be signed or unsigned, depending on platform). On /// Windows, this is `UInt16` (a "wide" character). public typealias PlatformChar = UInt16 - #else +#else /// The platform's preferred character type. On Unix, this is an 8-bit C /// `char` (which may be signed or unsigned, depending on platform). On /// Windows, this is `UInt16` (a "wide" character). public typealias PlatformChar = CInterop.Char - #endif +#endif - #if os(Windows) +#if os(Windows) /// The platform's preferred Unicode encoding. On Unix this is UTF-8 and on /// Windows it is UTF-16. Native strings may contain invalid Unicode, /// which will be handled by either error-correction or failing, depending /// on API. public typealias PlatformUnicodeEncoding = UTF16 - #else +#else /// The platform's preferred Unicode encoding. On Unix this is UTF-8 and on /// Windows it is UTF-16. Native strings may contain invalid Unicode, /// which will be handled by either error-correction or failing, depending /// on API. public typealias PlatformUnicodeEncoding = UTF8 +#endif + +#if !os(Windows) + /// The C `struct flock` type + public typealias FileLock = flock + + /// The C `pid_t` type + public typealias PID = pid_t + + /// The C `off_t` type. + /// + /// Note System generally standardizes on `Int64` where `off_t` + /// might otherwise appear. This typealias allows conversion code to be + /// emitted into client. + public typealias Offset = off_t + + #if !os(Linux) + /// The C `fstore` type + public typealias FStore = fstore + + /// The C `fpunchhole` type + public typealias FPunchhole = fpunchhole + + /// The C `radvisory` type + public typealias RAdvisory = radvisory + + /// The C `radvisory` type + public typealias Log2Phys = log2phys #endif + +#endif // !os(Windows) + } + + diff --git a/Sources/System/Internals/Constants.swift b/Sources/System/Internals/Constants.swift index 85f9f3de..02eca1aa 100644 --- a/Sources/System/Internals/Constants.swift +++ b/Sources/System/Internals/Constants.swift @@ -531,3 +531,59 @@ internal var _SEEK_HOLE: CInt { SEEK_HOLE } @_alwaysEmitIntoClient internal var _SEEK_DATA: CInt { SEEK_DATA } #endif + +#if !os(Windows) +@_alwaysEmitIntoClient +internal var _LOCK_SH: CInt { LOCK_SH } + +@_alwaysEmitIntoClient +internal var _LOCK_EX: CInt { LOCK_EX } + +@_alwaysEmitIntoClient +internal var _LOCK_NB: CInt { LOCK_NB } + +@_alwaysEmitIntoClient +internal var _LOCK_UN: CInt { LOCK_UN } +#endif + +#if !os(Windows) +@_alwaysEmitIntoClient +internal var _F_OFD_SETLK: CInt { +#if os(Linux) + 37 // FIXME: F_OFD_SETLK +#else + 90 // FIXME: use API when available +#endif +} + +@_alwaysEmitIntoClient +internal var _F_OFD_SETLKW: CInt { +#if os(Linux) + 38 // FIXME: F_OFD_SETLKW +#else + 91 // FIXME: use API when available +#endif +} + +@_alwaysEmitIntoClient +internal var _F_OFD_GETLK: CInt { +#if os(Linux) + 36// FIXME: F_OFD_GETLK +#else + 92 // FIXME: use API when available +#endif +} + +#if !os(Linux) +@_alwaysEmitIntoClient +internal var _F_OFD_SETLKWTIMEOUT: CInt { + 93 // FIXME: use API when available +} +@_alwaysEmitIntoClient +internal var _F_OFD_GETLKPID: CInt { + 94 // FIXME: use API when available +} +#endif // !os(Linux) + +#endif // !os(Windows) + diff --git a/Sources/System/Internals/Syscalls.swift b/Sources/System/Internals/Syscalls.swift index c5c376c3..02754f59 100644 --- a/Sources/System/Internals/Syscalls.swift +++ b/Sources/System/Internals/Syscalls.swift @@ -133,3 +133,37 @@ internal func system_ftruncate(_ fd: Int32, _ length: off_t) -> Int32 { return ftruncate(fd, length) } #endif + +#if !os(Windows) +internal func system_flock(_ fd: Int32, _ operation: Int32) -> Int32 { +#if ENABLE_MOCKING + if mockingEnabled { return _mock(fd, operation) } +#endif + return flock(fd, operation) +} +#endif + +#if !os(Windows) +internal func system_fcntl(_ fd: Int32, _ cmd: Int32) -> Int32 { + #if ENABLE_MOCKING + if mockingEnabled { return _mock(fd, cmd) } + #endif + return fcntl(fd, cmd) + } + + internal func system_fcntl(_ fd: Int32, _ cmd: Int32, _ arg: Int32) -> Int32 { + #if ENABLE_MOCKING + if mockingEnabled { return _mock(fd, cmd, arg) } + #endif + return fcntl(fd, cmd, arg) + } + + internal func system_fcntl( + _ fd: Int32, _ cmd: Int32, _ arg: UnsafeMutableRawPointer + ) -> Int32 { + #if ENABLE_MOCKING + if mockingEnabled { return _mock(fd, cmd, arg) } + #endif + return fcntl(fd, cmd, arg) + } +#endif diff --git a/Sources/System/ProcessID.swift b/Sources/System/ProcessID.swift new file mode 100644 index 00000000..2f8ff9ab --- /dev/null +++ b/Sources/System/ProcessID.swift @@ -0,0 +1,22 @@ + +#if !os(Windows) + +/// The process identifier (aka PID) used to uniquely identify an active process. +/// +/// The corresponding C type is `pid_t` +@frozen +public struct ProcessID: RawRepresentable, Hashable, Sendable { + @_alwaysEmitIntoClient + public var rawValue: CInterop.PID + + @_alwaysEmitIntoClient + public init(rawValue: CInterop.PID) { + // We assert instead of precondition, as the user may want to + // use this to ferry a PIDOrPGID value (denoted by a negative number). + // They would a `EINVAL` on use, instead of trapping the process. + assert(rawValue >= 0, "Process IDs are always positive") + self.rawValue = rawValue + } +} + +#endif diff --git a/Sources/System/Util.swift b/Sources/System/Util.swift index 3a8df9ac..ce956fd1 100644 --- a/Sources/System/Util.swift +++ b/Sources/System/Util.swift @@ -43,6 +43,16 @@ internal func nothingOrErrno( valueOrErrno(retryOnInterrupt: retryOnInterrupt, f).map { _ in () } } +/// Promote `Errno.wouldBlock` / `Errno.resourceTemporarilyUnavailable` to `nil`. +internal func _extractWouldBlock( + _ value: Result +) -> Result? { + if case .failure(let err) = value, err == .wouldBlock { + return nil + } + return value +} + // Run a precondition for debug client builds internal func _debugPrecondition( _ condition: @autoclosure () -> Bool, @@ -128,6 +138,41 @@ extension MutableCollection where Element: Equatable { } } +/// Map byte offsets passed as a range expression to start+length, e.g. for +/// use in `struct flock`. +/// +/// Start can be negative, e.g. for use with `SEEK_CUR`. +/// +/// Convention: Half-open ranges or explicit `Int64.min` / `Int64.max` bounds +/// denote start or end. +/// +/// Passing `Int64.min` as the lower bound maps to a start offset of `0`, such +/// that `..<5` would map to `(start: 0, length: 5)`. +/// +/// Passing `Int64.max` as an upper bound maps to a length of `0` (i.e. rest +/// of file by convention), such that passing `5...` would map to `(start: 5, +/// length: 0)`. +/// +/// NOTE: This is a utility function and can return negative start offsets and +/// negative lengths E.g. `(-3)...` for user with `SEEK_CUR` and `...(-3)` +/// (TBD). It's up to the caller to check any additional invariants +/// +@_alwaysEmitIntoClient +internal func _mapByteRangeToByteOffsets( + _ byteRange: (some RangeExpression)? +) -> (start: Int64, length: Int64) { + let allInts = Int64.min..( to value: T?, _ body: (UnsafePointer?) throws -> R diff --git a/Tests/SystemTests/FileLockTest.swift b/Tests/SystemTests/FileLockTest.swift new file mode 100644 index 00000000..35e4ef75 --- /dev/null +++ b/Tests/SystemTests/FileLockTest.swift @@ -0,0 +1,81 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2020 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information +*/ + +import XCTest + +#if SYSTEM_PACKAGE +@testable import SystemPackage +#else +@testable import System +#endif + +func _range(_ r: some RangeExpression) -> Range { + r.relative(to: Int64.min.. A(write) -> FAIL: B(read/write) + XCTAssertTrue(try ofd_A.lock(.read)) + XCTAssertTrue(try ofd_A.lock(.write)) + XCTAssertTrue(try dup_A.lock(.write)) // redundant, but works + XCTAssertFalse(try ofd_B.lock(.read)) + XCTAssertFalse(try ofd_B.lock(.write)) + XCTAssertFalse(try dup_B.lock(.write)) + try dup_A.unlock() + + // A(read) -> B(read) -> FAIL: A/B(write) + // -> B(unlock) -> A(write) -> FAIL: B(read/write) + XCTAssertTrue(try dup_A.lock(.read)) + XCTAssertTrue(try ofd_B.lock(.read)) + XCTAssertFalse(try ofd_A.lock(.write)) + XCTAssertFalse(try dup_A.lock(.write)) + XCTAssertFalse(try ofd_B.lock(.write)) + XCTAssertFalse(try dup_B.lock(.write)) + try dup_B.unlock() + XCTAssertTrue(try ofd_A.lock(.write)) + XCTAssertFalse(try dup_B.lock(.read)) + XCTAssertFalse(try ofd_B.lock(.write)) + try dup_A.unlock() + + /// Byte ranges + + // A(read, ..<50) -> B(write, 50...) + // -> A(write, 10..<20) -> B(read, 40..<50) + // -> FAIL: B(read, 17..<18), A(read 60..<70) + // -> A(unlock, 11..<12) -> B(read, 11..<12) -> A(read, 11..<12) + // -> FAIL A/B(write, 11..<12) + XCTAssertTrue(try ofd_A.lock(.read, byteRange: ..<50)) + XCTAssertTrue(try ofd_B.lock(.write, byteRange: 50...)) + XCTAssertTrue(try ofd_A.lock(.write, byteRange: 10..<20)) + XCTAssertTrue(try ofd_B.lock(.read, byteRange: 40..<50)) + XCTAssertFalse(try ofd_B.lock(.read, byteRange: 17..<18)) + XCTAssertFalse(try ofd_A.lock(.read, byteRange: 60..<70)) + try dup_A.unlock(byteRange: 11..<12) + XCTAssertTrue(try ofd_B.lock(.read, byteRange: 11..<12)) + XCTAssertTrue(try ofd_A.lock(.read, byteRange: 11..<12)) + XCTAssertFalse(try ofd_B.lock(.write, byteRange: 11..<12)) + XCTAssertFalse(try ofd_A.lock(.write, byteRange: 11..<12)) + } + + func testFileLocksWaiting() { + // TODO: Test waiting, test waiting until timeouts + } +} + diff --git a/Tests/SystemTests/FileOperationsTest.swift b/Tests/SystemTests/FileOperationsTest.swift index 8062aedc..05c2d220 100644 --- a/Tests/SystemTests/FileOperationsTest.swift +++ b/Tests/SystemTests/FileOperationsTest.swift @@ -28,7 +28,7 @@ final class FileOperationsTest: XCTestCase { let writeBuf = UnsafeRawBufferPointer(rawBuf) let writeBufAddr = writeBuf.baseAddress - let syscallTestCases: Array = [ + var syscallTestCases: Array = [ MockTestCase(name: "open", .interruptable, "a path", O_RDWR | O_APPEND) { retryOnInterrupt in _ = try FileDescriptor.open( diff --git a/Tests/SystemTests/InternalUnitTests.swift b/Tests/SystemTests/InternalUnitTests.swift new file mode 100644 index 00000000..2a08b6ed --- /dev/null +++ b/Tests/SystemTests/InternalUnitTests.swift @@ -0,0 +1,52 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2020 - 2021 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information +*/ + +import XCTest + +#if SYSTEM_PACKAGE +@testable import SystemPackage +#else +@testable import System +#endif + +final class InternalUnitTests: XCTestCase { + + + func testFileOffsets() { + func test( + _ br: (some RangeExpression)?, + _ expected: (start: Int64, length: Int64) + ) { + let (start, len) = _mapByteRangeToByteOffsets(br) + XCTAssertEqual(start, expected.start) + XCTAssertEqual(len, expected.length) + } + + test(2..<5, (start: 2, length: 3)) + test(2...5, (start: 2, length: 4)) + + test(..<5, (start: 0, length: 5)) + test(...5, (start: 0, length: 6)) + test(5..., (start: 5, length: 0)) + + // E.g. for use in specifying n bytes behind SEEK_CUR + // + // FIXME: are these ok? the API is for absolute + // offsets... + test((-3)..., (start: -3, length: 0)) + test((-3)..<0, (start: -3, length: 3)) + + // Non-sensical: up to the caller + test(..<(-5), (start: 0, length: -5)) + + } + + + +} diff --git a/Tests/SystemTests/MockingTest.swift b/Tests/SystemTests/MockingTest.swift index 1f2c96da..0b22832a 100644 --- a/Tests/SystemTests/MockingTest.swift +++ b/Tests/SystemTests/MockingTest.swift @@ -47,4 +47,13 @@ final class MockingTest: XCTestCase { } XCTAssertFalse(mockingEnabled) } + + func testFCNTLMocking() { + MockingDriver.withMockingEnabled { driver in + XCTAssertTrue(mockingEnabled) + + // TODO: a handful of mock tests, especially those that have different number of parameters + } + + } } diff --git a/Tests/SystemTests/XCTestManifests.swift b/Tests/SystemTests/XCTestManifests.swift index de99bd81..b096f265 100644 --- a/Tests/SystemTests/XCTestManifests.swift +++ b/Tests/SystemTests/XCTestManifests.swift @@ -1,4 +1,4 @@ -#if !canImport(ObjectiveC) && swift(<5.5) +#if !canImport(ObjectiveC) import XCTest extension ErrnoTest { @@ -28,8 +28,11 @@ extension FileOperationsTest { static let __allTests__FileOperationsTest = [ ("testAdHocOpen", testAdHocOpen), ("testAdHocPipe", testAdHocPipe), + ("testFileLocks", testFileLocks), + ("testFileLocksWaiting", testFileLocksWaiting), ("testGithubIssues", testGithubIssues), ("testHelpers", testHelpers), + ("testResizeFile", testResizeFile), ("testSyscalls", testSyscalls), ] } @@ -86,11 +89,21 @@ extension FilePermissionsTest { ] } +extension InternalUnitTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__InternalUnitTests = [ + ("testFileOffsets", testFileOffsets), + ] +} + extension MockingTest { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` // to regenerate. static let __allTests__MockingTest = [ + ("testFCNTLMocking", testFCNTLMocking), ("testMocking", testMocking), ] } @@ -109,11 +122,36 @@ extension SystemStringTest { // `swift test --generate-linuxmain` // to regenerate. static let __allTests__SystemStringTest = [ + ("test_FilePath_initWithArrayConversion", test_FilePath_initWithArrayConversion), + ("test_FilePath_initWithInoutConversion", test_FilePath_initWithInoutConversion), + ("test_FilePath_initWithStringConversion", test_FilePath_initWithStringConversion), + ("test_FilePathComponent_initWithArrayConversion", test_FilePathComponent_initWithArrayConversion), + ("test_FilePathComponent_initWithInoutConversion", test_FilePathComponent_initWithInoutConversion), + ("test_FilePathComponent_initWithStringConversion", test_FilePathComponent_initWithStringConversion), + ("test_FilePathRoot_initWithArrayConversion", test_FilePathRoot_initWithArrayConversion), + ("test_FilePathRoot_initWithInoutConversion", test_FilePathRoot_initWithInoutConversion), + ("test_FilePathRoot_initWithStringConversion", test_FilePathRoot_initWithStringConversion), + ("test_String_initWithArrayConversion", test_String_initWithArrayConversion), + ("test_String_initWithInoutConversion", test_String_initWithInoutConversion), + ("test_String_initWithStringConversion", test_String_initWithStringConversion), + ("test_String_validatingPlatformStringWithArrayConversion", test_String_validatingPlatformStringWithArrayConversion), + ("test_String_validatingPlatformStringWithInoutConversion", test_String_validatingPlatformStringWithInoutConversion), + ("test_String_validatingPlatformStringWithStringConversion", test_String_validatingPlatformStringWithStringConversion), ("testAdHoc", testAdHoc), ("testPlatformString", testPlatformString), ] } +extension UtilTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__UtilTests = [ + ("testCStringArray", testCStringArray), + ("testStackBuffer", testStackBuffer), + ] +} + public func __allTests() -> [XCTestCaseEntry] { return [ testCase(ErrnoTest.__allTests__ErrnoTest), @@ -124,9 +162,11 @@ public func __allTests() -> [XCTestCaseEntry] { testCase(FilePathSyntaxTest.__allTests__FilePathSyntaxTest), testCase(FilePathTest.__allTests__FilePathTest), testCase(FilePermissionsTest.__allTests__FilePermissionsTest), + testCase(InternalUnitTests.__allTests__InternalUnitTests), testCase(MockingTest.__allTests__MockingTest), testCase(SystemCharTest.__allTests__SystemCharTest), testCase(SystemStringTest.__allTests__SystemStringTest), + testCase(UtilTests.__allTests__UtilTests), ] } #endif