diff --git a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift index d52fca85..b9506661 100644 --- a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift @@ -203,14 +203,29 @@ extension Sequence where Element == ParsableCommand.Type { } extension String { + #if canImport(FoundationEssentials) + func shellEscapeForSingleQuotedString(iterationCount: UInt64 = 1) -> Self { + iterationCount == 0 + ? self + : replacing("'", with: "'\\''") + .shellEscapeForSingleQuotedString(iterationCount: iterationCount - 1) + } + #else func shellEscapeForSingleQuotedString(iterationCount: UInt64 = 1) -> Self { iterationCount == 0 ? self : replacingOccurrences(of: "'", with: "'\\''") .shellEscapeForSingleQuotedString(iterationCount: iterationCount - 1) } - + #endif + + #if canImport(FoundationEssentials) + func shellEscapeForVariableName() -> Self { + replacing("-", with: "_") + } + #else func shellEscapeForVariableName() -> Self { replacingOccurrences(of: "-", with: "_") } + #endif } diff --git a/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift index 3054187b..db282f4f 100644 --- a/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift @@ -132,9 +132,15 @@ extension Name { } extension String { + #if canImport(FoundationEssentials) + fileprivate func fishEscape() -> String { + replacing("'", with: #"\'"#) + } + #else fileprivate func fishEscape() -> String { replacingOccurrences(of: "'", with: #"\'"#) } + #endif } extension FishCompletionsGenerator { diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index 826af9d2..988ade8f 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -221,14 +221,20 @@ extension [ParsableCommand.Type] { } extension String { + #if canImport(FoundationEssentials) + fileprivate func zshEscapeForSingleQuotedExplanation() -> String { + replacing(#/[\\\[\]]/#, with: { "\\\($0.output)" }) + .shellEscapeForSingleQuotedString() + } + #else fileprivate func zshEscapeForSingleQuotedExplanation() -> String { replacingOccurrences( of: #"[\\\[\]]"#, with: #"\\$0"#, - options: .regularExpression - ) + options: .regularExpression) .shellEscapeForSingleQuotedString() } + #endif } extension ArgumentDefinition { diff --git a/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift b/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift index 7a476999..1bbf9273 100644 --- a/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift +++ b/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift @@ -35,7 +35,7 @@ struct _WrappedParsableCommand: ParsableCommand { // If the type is named something like "TransformOptions", we only want // to use "transform" as the command name. - if let optionsRange = name.range(of: "_options"), + if let optionsRange = name._range(of: "_options"), optionsRange.upperBound == name.endIndex { return String(name[..=6.0) +#if canImport(FoundationEssentials) +internal import class FoundationEssentials.ProcessInfo +#else internal import class Foundation.ProcessInfo +#endif +#else +#if canImport(FoundationEssentials) +import class FoundationEssentials.ProcessInfo #else import class Foundation.ProcessInfo #endif +#endif + struct CommandError: Error { var commandStack: [ParsableCommand.Type] diff --git a/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift b/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift index dc4b6c00..e7dfa2fb 100644 --- a/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift +++ b/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift @@ -11,11 +11,19 @@ #if swift(>=6.0) internal import ArgumentParserToolInfo +#if canImport(FoundationEssentials) +internal import class FoundationEssentials.JSONEncoder +#else internal import class Foundation.JSONEncoder +#endif #else import ArgumentParserToolInfo +#if canImport(FoundationEssentials) +import class FoundationEssentials.JSONEncoder +#else import class Foundation.JSONEncoder #endif +#endif internal struct DumpHelpGenerator { private var toolInfo: ToolInfoV0 diff --git a/Sources/ArgumentParser/Usage/MessageInfo.swift b/Sources/ArgumentParser/Usage/MessageInfo.swift index 4e42b460..ed22cdf5 100644 --- a/Sources/ArgumentParser/Usage/MessageInfo.swift +++ b/Sources/ArgumentParser/Usage/MessageInfo.swift @@ -10,12 +10,20 @@ //===----------------------------------------------------------------------===// #if swift(>=6.0) +#if canImport(FoundationEssentials) +internal import protocol FoundationEssentials.LocalizedError +#else internal import protocol Foundation.LocalizedError internal import class Foundation.NSError +#endif +#else +#if canImport(FoundationEssentials) +import protocol FoundationEssentials.LocalizedError #else import protocol Foundation.LocalizedError import class Foundation.NSError #endif +#endif enum MessageInfo { case help(text: String) @@ -135,11 +143,15 @@ enum MessageInfo { // No way to unwrap bind description in pattern self = .other(message: error.errorDescription!, exitCode: .failure) default: + #if canImport(FoundationEssentials) + self = .other(message: String(describing: error), exitCode: .failure) + #else if Swift.type(of: error) is NSError.Type { self = .other(message: error.localizedDescription, exitCode: .failure) } else { self = .other(message: String(describing: error), exitCode: .failure) } + #endif } } else if let parserError = parserError { let usage: String = { diff --git a/Sources/ArgumentParser/Usage/UsageGenerator.swift b/Sources/ArgumentParser/Usage/UsageGenerator.swift index 9c628c0c..d1e74580 100644 --- a/Sources/ArgumentParser/Usage/UsageGenerator.swift +++ b/Sources/ArgumentParser/Usage/UsageGenerator.swift @@ -10,10 +10,18 @@ //===----------------------------------------------------------------------===// #if swift(>=6.0) +#if canImport(FoundationEssentials) +internal import protocol FoundationEssentials.LocalizedError +#else internal import protocol Foundation.LocalizedError +#endif +#else +#if canImport(FoundationEssentials) +import protocol FoundationEssentials.LocalizedError #else import protocol Foundation.LocalizedError #endif +#endif struct UsageGenerator { var toolName: String diff --git a/Sources/ArgumentParser/Utilities/BidirectionalCollection.swift b/Sources/ArgumentParser/Utilities/BidirectionalCollection.swift new file mode 100644 index 00000000..32710e2f --- /dev/null +++ b/Sources/ArgumentParser/Utilities/BidirectionalCollection.swift @@ -0,0 +1,82 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + + +extension BidirectionalCollection { + // Equal to calling `index(&idx, offsetBy: -other.count)` with just one loop + func _index(_ index: Index, backwardsOffsetByCountOf other: S) -> Index? { + var idx = index + var otherIdx = other.endIndex + while otherIdx > other.startIndex { + guard idx > startIndex else { + // other.count > self.count: bail + return nil + } + other.formIndex(before: &otherIdx) + formIndex(before: &idx) + } + return idx + } + + func _range(of other: S, anchored: Bool = false, backwards: Bool = false) -> Range? where S.Element == Element, Element : Equatable { + var result: Range? = nil + var fromLoc: Index + var toLoc: Index + if backwards { + guard let idx = _index(endIndex, backwardsOffsetByCountOf: other) else { + // other.count > string.count: bail + return nil + } + fromLoc = idx + + toLoc = anchored ? fromLoc : startIndex + } else { + fromLoc = startIndex + if anchored { + toLoc = fromLoc + } else { + guard let idx = _index(endIndex, backwardsOffsetByCountOf: other) else { + return nil + } + toLoc = idx + } + } + + let delta = fromLoc <= toLoc ? 1 : -1 + + while true { + var str1Index = fromLoc + var str2Index = other.startIndex + + while str2Index < other.endIndex && str1Index < endIndex { + if self[str1Index] != other[str2Index] { + break + } + formIndex(after: &str1Index) + other.formIndex(after: &str2Index) + } + + if str2Index == other.endIndex { + result = fromLoc..=6.0) -internal import Foundation -#else -import Foundation +#if !canImport(FoundationEssentials) +import class Foundation.NSLock #endif /// A synchronization primitive that protects shared mutable state via mutual @@ -22,9 +20,13 @@ import Foundation /// protecting by blocking threads attempting to acquire the lock. Only one /// execution context at a time has access to the value stored within the /// `Mutex` allowing for exclusive access. -class Mutex: @unchecked Sendable { +final class Mutex: @unchecked Sendable { /// The lock used to synchronize access to the value. + #if canImport(FoundationEssentials) + var lock: NIOLock + #else var lock: NSLock + #endif /// The value protected by the mutex. var value: T @@ -58,3 +60,268 @@ class Mutex: @unchecked Sendable { return try body(&self.value) } } + +#if canImport(FoundationEssentials) + +#if os(Windows) +import ucrt +import WinSDK +#elseif canImport(Glibc) +@preconcurrency import Glibc +#elseif canImport(Musl) +@preconcurrency import Musl +#elseif canImport(Bionic) +@preconcurrency import Bionic +#elseif canImport(WASILibc) +@preconcurrency import WASILibc +#if canImport(wasi_pthread) +import wasi_pthread +#endif +#else +#error("The concurrency NIOLock module was unable to identify your C library.") +#endif + +#if os(Windows) +@usableFromInline +typealias LockPrimitive = SRWLOCK +#else +@usableFromInline +typealias LockPrimitive = pthread_mutex_t +#endif + +@usableFromInline +enum LockOperations {} + +extension LockOperations { + @inlinable + static func create(_ mutex: UnsafeMutablePointer) { + mutex.assertValidAlignment() + + #if os(Windows) + InitializeSRWLock(mutex) + #elseif !os(WASI) + var attr = pthread_mutexattr_t() + pthread_mutexattr_init(&attr) + debugOnly { + pthread_mutexattr_settype(&attr, .init(PTHREAD_MUTEX_ERRORCHECK)) + } + + let err = pthread_mutex_init(mutex, &attr) + precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") + #endif + } + + @inlinable + static func destroy(_ mutex: UnsafeMutablePointer) { + mutex.assertValidAlignment() + + #if os(Windows) + // SRWLOCK does not need to be free'd + #elseif !os(WASI) + let err = pthread_mutex_destroy(mutex) + precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") + #endif + } + + @inlinable + static func lock(_ mutex: UnsafeMutablePointer) { + mutex.assertValidAlignment() + + #if os(Windows) + AcquireSRWLockExclusive(mutex) + #elseif !os(WASI) + let err = pthread_mutex_lock(mutex) + precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") + #endif + } + + @inlinable + static func unlock(_ mutex: UnsafeMutablePointer) { + mutex.assertValidAlignment() + + #if os(Windows) + ReleaseSRWLockExclusive(mutex) + #elseif !os(WASI) + let err = pthread_mutex_unlock(mutex) + precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") + #endif + } +} + +// Tail allocate both the mutex and a generic value using ManagedBuffer. +// Both the header pointer and the elements pointer are stable for +// the class's entire lifetime. +// +// However, for safety reasons, we elect to place the lock in the "elements" +// section of the buffer instead of the head. The reasoning here is subtle, +// so buckle in. +// +// _As a practical matter_, the implementation of ManagedBuffer ensures that +// the pointer to the header is stable across the lifetime of the class, and so +// each time you call `withUnsafeMutablePointers` or `withUnsafeMutablePointerToHeader` +// the value of the header pointer will be the same. This is because ManagedBuffer uses +// `Builtin.addressOf` to load the value of the header, and that does ~magic~ to ensure +// that it does not invoke any weird Swift accessors that might copy the value. +// +// _However_, the header is also available via the `.header` field on the ManagedBuffer. +// This presents a problem! The reason there's an issue is that `Builtin.addressOf` and friends +// do not interact with Swift's exclusivity model. That is, the various `with` functions do not +// conceptually trigger a mutating access to `.header`. For elements this isn't a concern because +// there's literally no other way to perform the access, but for `.header` it's entirely possible +// to accidentally recursively read it. +// +// Our implementation is free from these issues, so we don't _really_ need to worry about it. +// However, out of an abundance of caution, we store the Value in the header, and the LockPrimitive +// in the trailing elements. We still don't use `.header`, but it's better to be safe than sorry, +// and future maintainers will be happier that we were cautious. +// +// See also: https://github.com/apple/swift/pull/40000 +@usableFromInline +final class LockStorage: ManagedBuffer { + + @inlinable + static func create(value: Value) -> Self { + let buffer = Self.create(minimumCapacity: 1) { _ in + value + } + // Intentionally using a force cast here to avoid a miss compiliation in 5.10. + // This is as fast as an unsafeDownCast since ManagedBuffer is inlined and the optimizer + // can eliminate the upcast/downcast pair + let storage = buffer as! Self + + storage.withUnsafeMutablePointers { _, lockPtr in + LockOperations.create(lockPtr) + } + + return storage + } + + @inlinable + func lock() { + self.withUnsafeMutablePointerToElements { lockPtr in + LockOperations.lock(lockPtr) + } + } + + @inlinable + func unlock() { + self.withUnsafeMutablePointerToElements { lockPtr in + LockOperations.unlock(lockPtr) + } + } + + @inlinable + deinit { + self.withUnsafeMutablePointerToElements { lockPtr in + LockOperations.destroy(lockPtr) + } + } + + @inlinable + func withLockPrimitive(_ body: (UnsafeMutablePointer) throws -> T) rethrows -> T { + try self.withUnsafeMutablePointerToElements { lockPtr in + try body(lockPtr) + } + } + + @inlinable + func withLockedValue(_ mutate: (inout Value) throws -> T) rethrows -> T { + try self.withUnsafeMutablePointers { valuePtr, lockPtr in + LockOperations.lock(lockPtr) + defer { LockOperations.unlock(lockPtr) } + return try mutate(&valuePtr.pointee) + } + } +} + +/// A threading lock based on `libpthread` instead of `libdispatch`. +/// +/// - Note: ``NIOLock`` has reference semantics. +/// +/// This object provides a lock on top of a single `pthread_mutex_t`. This kind +/// of lock is safe to use with `libpthread`-based threading models, such as the +/// one used by NIO. On Windows, the lock is based on the substantially similar +/// `SRWLOCK` type. +public struct NIOLock { + @usableFromInline + internal let _storage: LockStorage + + /// Create a new lock. + @inlinable + public init() { + self._storage = .create(value: ()) + } + + /// Acquire the lock. + /// + /// Whenever possible, consider using `withLock` instead of this method and + /// `unlock`, to simplify lock handling. + @inlinable + public func lock() { + self._storage.lock() + } + + /// Release the lock. + /// + /// Whenever possible, consider using `withLock` instead of this method and + /// `lock`, to simplify lock handling. + @inlinable + public func unlock() { + self._storage.unlock() + } + + @inlinable + internal func withLockPrimitive(_ body: (UnsafeMutablePointer) throws -> T) rethrows -> T { + try self._storage.withLockPrimitive(body) + } +} + +extension NIOLock { + /// Acquire the lock for the duration of the given block. + /// + /// This convenience method should be preferred to `lock` and `unlock` in + /// most situations, as it ensures that the lock will be released regardless + /// of how `body` exits. + /// + /// - Parameter body: The block to execute while holding the lock. + /// - Returns: The value returned by the block. + @inlinable + public func withLock(_ body: () throws -> T) rethrows -> T { + self.lock() + defer { + self.unlock() + } + return try body() + } + + @inlinable + public func withLockVoid(_ body: () throws -> Void) rethrows { + try self.withLock(body) + } +} + +extension NIOLock: @unchecked Sendable {} + +extension UnsafeMutablePointer { + @inlinable + func assertValidAlignment() { + assert(UInt(bitPattern: self) % UInt(MemoryLayout.alignment) == 0) + } +} + +/// A utility function that runs the body code only in debug builds, without +/// emitting compiler warnings. +/// +/// This is currently the only way to do this in Swift: see +/// https://forums.swift.org/t/support-debug-only-code/11037 for a discussion. +@inlinable +internal func debugOnly(_ body: () -> Void) { + assert( + { + body() + return true + }() + ) +} + +#endif \ No newline at end of file diff --git a/Tests/ArgumentParserUnitTests/ExitCodeTests.swift b/Tests/ArgumentParserUnitTests/ExitCodeTests.swift index fbe62da7..138db1c9 100644 --- a/Tests/ArgumentParserUnitTests/ExitCodeTests.swift +++ b/Tests/ArgumentParserUnitTests/ExitCodeTests.swift @@ -83,7 +83,10 @@ extension ExitCodeTests { // MARK: - NSError tests extension ExitCodeTests { - func testNSErrorIsHandled() { + func testNSErrorIsHandled() throws{ + #if canImport(FoundationEssentials) + throw XCTSkip("FoundationEssentials doesn't have NSError") + #else struct NSErrorCommand: ParsableCommand { static let fileNotFoundNSError = NSError( domain: "", code: 1, @@ -98,5 +101,6 @@ extension ExitCodeTests { XCTAssertEqual( NSErrorCommand.message(for: NSErrorCommand.fileNotFoundNSError), "The file “foo/bar” couldn’t be opened because there is no such file") + #endif } }