diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f5f9698..56cf53e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,9 +14,11 @@ jobs: contents: read uses: vapor/ci/.github/workflows/run-unit-tests.yml@main with: + with_release_mode_testing: true with_windows: true with_musl: true with_android: true + with_wasm: true with_linting: true enable_all_traits: true secrets: inherit @@ -39,6 +41,13 @@ jobs: uses: vapor/ci/.github/workflows/submit-deps.yml@main secrets: inherit + foundation-linking: + uses: vapor/ci/.github/workflows/check-foundation-linking.yml@main + permissions: + contents: read + with: + swift_image: swift:6.3-noble + # integration-check: # runs-on: ubuntu-latest # container: swift:noble diff --git a/Package.swift b/Package.swift index 8d29cb5..c120dbb 100644 --- a/Package.swift +++ b/Package.swift @@ -66,6 +66,12 @@ let package = Package( var swiftSettings: [SwiftSetting] { [ + .treatAllWarnings(as: .error), + .strictMemorySafety(), + .enableExperimentalFeature("SuppressedAssociatedTypesWithDefaults"), + .enableExperimentalFeature("LifetimeDependence"), + .enableExperimentalFeature("Lifetimes"), + .enableUpcomingFeature("LifetimeDependence"), .enableUpcomingFeature("ExistentialAny"), .enableUpcomingFeature("InternalImportsByDefault"), .enableUpcomingFeature("MemberImportVisibility"), diff --git a/Sources/ConsoleKit/Terminal/ANSI.swift b/Sources/ConsoleKit/Terminal/ANSI.swift index 44f38b5..63306aa 100644 --- a/Sources/ConsoleKit/Terminal/ANSI.swift +++ b/Sources/ConsoleKit/Terminal/ANSI.swift @@ -1,11 +1,11 @@ #if canImport(Darwin) import Darwin.C #elseif canImport(Glibc) -@preconcurrency import Glibc +@unsafe @preconcurrency import Glibc #elseif canImport(Musl) -@preconcurrency import Musl +@unsafe @preconcurrency import Musl #elseif canImport(Android) -@preconcurrency import Android +@unsafe @preconcurrency import Android #elseif os(WASI) import WASILibc #elseif os(Windows) @@ -61,9 +61,9 @@ extension Terminal { // fdopen() on stdout is fast; also the returned file MUST NOT be fclose()d // This avoids concurrency complaints due to accessing global `stdout`. #if os(Windows) - fflush(_fdopen(_fileno(stdout), "w+")) + unsafe fflush(_fdopen(_fileno(stdout), "w+")) #else - fflush(fdopen(STDOUT_FILENO, "w+")) + unsafe fflush(fdopen(STDOUT_FILENO, "w+")) #endif } } diff --git a/Sources/ConsoleKit/Terminal/Console.swift b/Sources/ConsoleKit/Terminal/Console.swift index 1f85d21..e634f5f 100644 --- a/Sources/ConsoleKit/Terminal/Console.swift +++ b/Sources/ConsoleKit/Terminal/Console.swift @@ -1,11 +1,11 @@ #if canImport(Darwin) import Darwin.C #elseif canImport(Glibc) -@preconcurrency import Glibc +@unsafe @preconcurrency import Glibc #elseif canImport(Musl) -@preconcurrency import Musl +@unsafe @preconcurrency import Musl #elseif canImport(Android) -@preconcurrency import Android +@unsafe @preconcurrency import Android #elseif os(WASI) import WASILibc #elseif os(Windows) @@ -111,7 +111,7 @@ extension Console { // Xcode output does not support ANSI commands return false #elseif os(Windows) - return _isatty(_fileno(stdout)) > 0 + return unsafe _isatty(_fileno(stdout)) > 0 #else // If STDOUT is not an interactive terminal then omit ANSI commands return isatty(STDOUT_FILENO) > 0 diff --git a/Sources/ConsoleKit/Terminal/Terminal.swift b/Sources/ConsoleKit/Terminal/Terminal.swift index 0845d46..76eb6a3 100644 --- a/Sources/ConsoleKit/Terminal/Terminal.swift +++ b/Sources/ConsoleKit/Terminal/Terminal.swift @@ -3,11 +3,11 @@ #if canImport(Darwin) import Darwin.C #elseif canImport(Glibc) -@preconcurrency import Glibc +@unsafe @preconcurrency import Glibc #elseif canImport(Musl) -@preconcurrency import Musl +@unsafe @preconcurrency import Musl #elseif canImport(Android) -@preconcurrency import Android +@unsafe @preconcurrency import Android #elseif os(WASI) import WASILibc #elseif os(Windows) @@ -66,26 +66,26 @@ public final class Terminal: Console, Sendable { // swift-format-ignore func plat_readpassphrase(into buf: UnsafeMutableBufferPointer) -> Int { #if canImport(Darwin) - let rpp = readpassphrase + let rpp = unsafe readpassphrase #else - let rpp = linux_readpassphrase + let rpp = unsafe linux_readpassphrase let RPP_REQUIRE_TTY = 0 as Int32 #endif - while rpp("", buf.baseAddress!, buf.count, RPP_REQUIRE_TTY) == nil { + while unsafe rpp("", buf.baseAddress!, buf.count, RPP_REQUIRE_TTY) == nil { guard errno == EINTR else { return 0 } } - return strlen(buf.baseAddress!) + return unsafe strlen(buf.baseAddress!) } // swift-format-ignore func readpassphrase_str() -> String { if #available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) { - return .init(unsafeUninitializedCapacity: 1024) { - $0.withMemoryRebound(to: Int8.self) { plat_readpassphrase(into: $0) } + return unsafe .init(unsafeUninitializedCapacity: 1024) { + unsafe $0.withMemoryRebound(to: Int8.self) { unsafe plat_readpassphrase(into: $0) } } } else { - return .init( - decoding: [Int8](unsafeUninitializedCapacity: 1024) { $1 = plat_readpassphrase(into: $0) }.map(UInt8.init), + return unsafe .init( + decoding: [Int8](unsafeUninitializedCapacity: 1024) { $1 = unsafe plat_readpassphrase(into: $0) }.map(UInt8.init), as: UTF8.self ) } @@ -140,16 +140,16 @@ public final class Terminal: Console, Sendable { output = text.description } Swift.print(output, terminator: newLine ? "\n" : "") - fflush(stdout) + unsafe fflush(stdout) } /// See ``Console`` public func report(error: String, newLine: Bool) { for c in (newLine ? "\(error)\n" : error).utf8 { #if os(Windows) - _putc_nolock(CInt(c), stderr) + unsafe _putc_nolock(CInt(c), stderr) #else - putc_unlocked(CInt(c), stderr) + unsafe putc_unlocked(CInt(c), stderr) #endif } } @@ -158,11 +158,14 @@ public final class Terminal: Console, Sendable { public var size: (width: Int, height: Int) { #if os(Windows) var csbi = CONSOLE_SCREEN_BUFFER_INFO() - GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi) + unsafe GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi) return (Int(csbi.dwSize.X), Int(csbi.dwSize.Y)) + #elseif os(WASI) + // WASI has no `ioctl`/`TIOCGWINSZ`/`winsize`, so return `0, 0` as "unknown size". + return (0, 0) #else var w = winsize() - _ = ioctl(STDOUT_FILENO, UInt(TIOCGWINSZ), &w) + _ = unsafe ioctl(STDOUT_FILENO, UInt(TIOCGWINSZ), &w) return (Int(w.ws_col), Int(w.ws_row)) #endif } diff --git a/Sources/ConsoleKit/Terminal/readpassphrase_linux.swift b/Sources/ConsoleKit/Terminal/readpassphrase_linux.swift index 89af717..08a1c76 100644 --- a/Sources/ConsoleKit/Terminal/readpassphrase_linux.swift +++ b/Sources/ConsoleKit/Terminal/readpassphrase_linux.swift @@ -4,11 +4,11 @@ #if canImport(Darwin) import Darwin #elseif canImport(Glibc) -@preconcurrency import Glibc +@unsafe @preconcurrency import Glibc #elseif canImport(Android) -@preconcurrency import Android +@unsafe @preconcurrency import Android #elseif canImport(Musl) -@preconcurrency import Musl +@unsafe @preconcurrency import Musl #endif /// This implementation of `readpassphrase()`, used only on Linux where it's extremely difficult to get at the `libbsd` @@ -36,33 +36,33 @@ internal func linux_readpassphrase( #endif // Open /dev/tty - let fd = open("/dev/tty", O_RDWR) + let fd = unsafe open("/dev/tty", O_RDWR) guard fd >= 0 else { return nil } defer { close(fd) } // Disable echo var oterm = termios() var term = termios() - guard tcgetattr(fd, &oterm) == 0 else { return nil } + guard unsafe tcgetattr(fd, &oterm) == 0 else { return nil } term = oterm if (flags & 0x1 /* RPP_ECHO_ON */) == 0 { term.c_lflag &= tcflag_t(bitPattern: numericCast(~(ECHO | ECHONL))) } - _ = tcsetattr(fd, TCSAFLUSH | TCSASOFT, &term) // libbsd ignores it if this calls fails, should we be doing the same? + _ = unsafe tcsetattr(fd, TCSAFLUSH | TCSASOFT, &term) // libbsd ignores it if this calls fails, should we be doing the same? // Reset the signal counts and install a recovery handler onto a whole buncha signals - linux_readpassphrase_signos.reset() + unsafe linux_readpassphrase_signos.reset() var sigrecovery = sigaction() - sigemptyset(&sigrecovery.sa_mask) + unsafe sigemptyset(&sigrecovery.sa_mask) sigrecovery.sa_flags = 0 #if canImport(Darwin) - sigrecovery.__sigaction_u = .init(__sa_handler: { linux_readpassphrase_signos[$0] += 1 }) + sigrecovery.__sigaction_u = .init(__sa_handler: { unsafe linux_readpassphrase_signos[$0] += 1 }) #elseif canImport(Glibc) - sigrecovery.__sigaction_handler = .init(sa_handler: { linux_readpassphrase_signos[$0] += 1 }) + sigrecovery.__sigaction_handler = .init(sa_handler: { unsafe linux_readpassphrase_signos[$0] += 1 }) #elseif canImport(Musl) - sigrecovery.__sa_handler = .init(sa_handler: { linux_readpassphrase_signos[$0] += 1 }) + sigrecovery.__sa_handler = .init(sa_handler: { unsafe linux_readpassphrase_signos[$0] += 1 }) #elseif os(Android) - sigrecovery.sa_handler = { linux_readpassphrase_signos[$0] += 1 } + sigrecovery.sa_handler = { unsafe linux_readpassphrase_signos[$0] += 1 } #endif let sigsaves = linux_readpassphrase_installHandlers(linux_readpassphrase_signals, &sigrecovery) @@ -72,24 +72,24 @@ internal func linux_readpassphrase( var save_errno = 0 as Int32 var c: Int8 = 0 while i < bufsiz - 1 && nr == 1 && c != 0x0a && c != 0x0d { - nr = read(fd, &c, 1) + nr = unsafe read(fd, &c, 1) if nr == 1 { - buf[i] = c + unsafe buf[i] = c i += 1 } } - buf[i] = 0 + unsafe buf[i] = 0 save_errno = errno // save off errno for later restoration after the state restoration stuff below - if (term.c_lflag & tcflag_t(bitPattern: numericCast(ECHO))) == 0 { write(fd, "\n", 1) } + if (term.c_lflag & tcflag_t(bitPattern: numericCast(ECHO))) == 0 { unsafe write(fd, "\n", 1) } // Restore original terminal config - if memcmp(&term, &oterm, MemoryLayout.size) != 0 { + if unsafe memcmp(&term, &oterm, MemoryLayout.size) != 0 { // I don't understand what this sequence accomplishes with respect to ignoring SIGTTOU. - let save_sigttou = linux_readpassphrase_signos[SIGTTOU] - while tcsetattr(fd, TCSAFLUSH | TCSASOFT, &oterm) == -1 && errno == EINTR && linux_readpassphrase_signos[SIGTTOU] == 0 { + let save_sigttou = unsafe linux_readpassphrase_signos[SIGTTOU] + while unsafe tcsetattr(fd, TCSAFLUSH | TCSASOFT, &oterm) == -1 && errno == EINTR && linux_readpassphrase_signos[SIGTTOU] == 0 { continue } - linux_readpassphrase_signos[SIGTTOU] = save_sigttou + unsafe linux_readpassphrase_signos[SIGTTOU] = save_sigttou } // Restore signal handlers @@ -98,7 +98,7 @@ internal func linux_readpassphrase( // libbsd closes the TTY fd here. Since we deferred the fd closure, we just hope the difference doesn't cause problems. // Re-raise any signals we temporarily ignored, now that the old signal handlers are back in place. - for i in 0.. init(capacity: I) { - self.capacity = Int(capacity) - self.baseAddress = .allocate(capacity: self.capacity) + unsafe self.capacity = Int(capacity) + unsafe self.baseAddress = .allocate(capacity: self.capacity) } subscript(_ index: Int) -> sig_atomic_t { - get { self.baseAddress.advanced(by: index).pointee } - nonmutating set { self.baseAddress.advanced(by: index).pointee = newValue } + get { unsafe self.baseAddress.advanced(by: index).pointee } + nonmutating set { unsafe self.baseAddress.advanced(by: index).pointee = newValue } } subscript(_ index: Int32) -> sig_atomic_t { - get { self[Int(index)] } - nonmutating set { self[Int(index)] = newValue } + get { unsafe self[Int(index)] } + nonmutating set { unsafe self[Int(index)] = newValue } } func reset() { - self.baseAddress.update(repeating: 0, count: self.capacity) + unsafe self.baseAddress.update(repeating: 0, count: self.capacity) } } #endif diff --git a/Sources/ConsoleLogger/ANSIColor.swift b/Sources/ConsoleLogger/ANSIColor.swift index 34eaf01..9fc1803 100644 --- a/Sources/ConsoleLogger/ANSIColor.swift +++ b/Sources/ConsoleLogger/ANSIColor.swift @@ -1,11 +1,11 @@ #if canImport(Darwin) import Darwin.C #elseif canImport(Glibc) -@preconcurrency import Glibc +@unsafe @preconcurrency import Glibc #elseif canImport(Musl) -@preconcurrency import Musl +@unsafe @preconcurrency import Musl #elseif canImport(Android) -@preconcurrency import Android +@unsafe @preconcurrency import Android #elseif os(WASI) import WASILibc #elseif os(Windows) @@ -32,7 +32,7 @@ private var supportsANSICommands: Bool { // Xcode output does not support ANSI commands return false #elseif os(Windows) - return _isatty(_fileno(stdout)) > 0 + return unsafe _isatty(_fileno(stdout)) > 0 #else // If STDOUT is not an interactive terminal then omit ANSI commands return isatty(STDOUT_FILENO) > 0 diff --git a/Sources/ConsoleLogger/LoggerFragments/LoggerFragment.swift b/Sources/ConsoleLogger/LoggerFragments/LoggerFragment.swift index b8406ae..52787ff 100644 --- a/Sources/ConsoleLogger/LoggerFragments/LoggerFragment.swift +++ b/Sources/ConsoleLogger/LoggerFragments/LoggerFragment.swift @@ -3,11 +3,11 @@ public import Logging #if canImport(Darwin) public import Darwin #elseif canImport(Glibc) -@preconcurrency public import Glibc +@unsafe @preconcurrency public import Glibc #elseif canImport(Musl) -@preconcurrency public import Musl +@unsafe @preconcurrency public import Musl #elseif canImport(Android) -@preconcurrency public import Android +@unsafe @preconcurrency public import Android #elseif os(WASI) public import WASILibc #elseif os(Windows) @@ -317,14 +317,15 @@ public struct SystemTimestampSource: TimestampSource { #if os(Windows) var timestamp = __time64_t() var localTime = tm() - _ = _time64(×tamp) - _ = _localtime64_s(&localTime, ×tamp) + _ = unsafe _time64(×tamp) + _ = unsafe _localtime64_s(&localTime, ×tamp) + return localTime #else var timestamp = time(nil) - var localTime = tm() - localtime_r(×tamp, &localTime) + var localTime = unsafe tm() + unsafe localtime_r(×tamp, &localTime) + return unsafe localTime #endif - return localTime } } @@ -342,13 +343,17 @@ public struct TimestampFragment: LoggerFragment { } private func timestamp() -> String { - withUnsafeTemporaryAllocation(of: CChar.self, capacity: 255) { + unsafe withUnsafeTemporaryAllocation(of: CChar.self, capacity: 255) { + #if os(Windows) var localTime = self.source.now() + #else + var localTime = unsafe self.source.now() + #endif - guard strftime($0.baseAddress!, $0.count, "%Y-%m-%dT%H:%M:%S%z", &localTime) > 0 else { + guard unsafe strftime($0.baseAddress!, $0.count, "%Y-%m-%dT%H:%M:%S%z", &localTime) > 0 else { return "" } - return String(cString: $0.baseAddress!) + return unsafe String(cString: $0.baseAddress!) } } } diff --git a/Tests/ConsoleKitTests/ReadPassphraseTests.swift b/Tests/ConsoleKitTests/ReadPassphraseTests.swift index eab5909..4163507 100644 --- a/Tests/ConsoleKitTests/ReadPassphraseTests.swift +++ b/Tests/ConsoleKitTests/ReadPassphraseTests.swift @@ -17,25 +17,25 @@ import Musl struct ReadPassphraseTests { private func handler(of sa: sigaction) -> UInt { #if canImport(Darwin) - return unsafeBitCast(sa.__sigaction_u.__sa_handler, to: UInt.self) + return unsafe unsafeBitCast(sa.__sigaction_u.__sa_handler, to: UInt.self) #elseif canImport(Glibc) - return unsafeBitCast(sa.__sigaction_handler.sa_handler, to: UInt.self) + return unsafe unsafeBitCast(sa.__sigaction_handler.sa_handler, to: UInt.self) #elseif canImport(Musl) - return unsafeBitCast(sa.__sa_handler.sa_handler, to: UInt.self) + return unsafe unsafeBitCast(sa.__sa_handler.sa_handler, to: UInt.self) #elseif os(Android) - return unsafeBitCast(sa.sa_handler, to: UInt.self) + return unsafe unsafeBitCast(sa.sa_handler, to: UInt.self) #endif } private func currentHandler(_ signo: Int32) -> UInt { var sa = sigaction() - sigaction(signo, nil, &sa) + unsafe sigaction(signo, nil, &sa) return self.handler(of: sa) } private func makeRecoveryHandler() -> sigaction { var sa = sigaction() - sigemptyset(&sa.sa_mask) + unsafe sigemptyset(&sa.sa_mask) sa.sa_flags = 0 #if canImport(Darwin) sa.__sigaction_u = .init(__sa_handler: { _ in }) diff --git a/Tests/ConsoleLoggerTests/LoggingTests.swift b/Tests/ConsoleLoggerTests/LoggingTests.swift index 65f0bd3..cb06765 100644 --- a/Tests/ConsoleLoggerTests/LoggingTests.swift +++ b/Tests/ConsoleLoggerTests/LoggingTests.swift @@ -5,11 +5,11 @@ import Testing #if canImport(Darwin) import Darwin.C #elseif canImport(Glibc) -@preconcurrency import Glibc +@unsafe @preconcurrency import Glibc #elseif canImport(Musl) -@preconcurrency import Musl +@unsafe @preconcurrency import Musl #elseif canImport(Android) -@preconcurrency import Android +@unsafe @preconcurrency import Android #elseif os(WASI) import WASILibc #elseif os(Windows) @@ -132,16 +132,17 @@ struct ConsoleLoggerTests { @Test("Timestamp Fragment") func timestampFragment() { - struct ConstantTimestampSource: TimestampSource, @unchecked Sendable { + @unsafe struct ConstantTimestampSource: TimestampSource, @unchecked Sendable { let time: tm func now() -> tm { - self.time + unsafe self.time } } let printer = TestingConsoleLoggerPrinter() let logger = Logger(label: "codes.vapor.console") { label in + #if os(Windows) var time = tm() time.tm_sec = 1 time.tm_min = 2 @@ -149,8 +150,17 @@ struct ConsoleLoggerTests { time.tm_mday = 4 time.tm_mon = 5 time.tm_year = 100 - - return ConsoleLogger( + #else + var time = unsafe tm() + unsafe time.tm_sec = 1 + unsafe time.tm_min = 2 + unsafe time.tm_hour = 3 + unsafe time.tm_mday = 4 + unsafe time.tm_mon = 5 + unsafe time.tm_year = 100 + #endif + + return unsafe ConsoleLogger( fragment: .timestampDefault(timestampSource: ConstantTimestampSource(time: time)), printer: printer, label: label