Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: CI

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
unit_tests:
strategy:
fail-fast: false
matrix:
os: [macos-latest, macos-15-intel, ubuntu-latest, ubuntu-24.04-arm]
traits: ["none", "Foundation"]
runs-on: ${{ matrix.os }}

steps:
- uses: actions/checkout@v6
- uses: SwiftyLab/setup-swift@latest
- name: Determine traits flags
id: traits
run: |
if [[ ${{ matrix.traits }} = 'all' ]]; then
echo "flags=--enable-all-traits" >> $GITHUB_OUTPUT
elif [[ ${{ matrix.traits }} = 'none' ]]; then
echo "flags=--disable-default-traits" >> $GITHUB_OUTPUT
else
echo "flags=--traits ${{ matrix.traits }}" >> $GITHUB_OUTPUT
fi
- name: Build
run: swift build --build-tests --enable-code-coverage -v ${{ steps.traits.outputs.flags }}
- name: Run tests
run: swift test --enable-code-coverage -v ${{ steps.traits.outputs.flags }} --xunit-output testresults.xml
- name: Collect artifacts
if: always()
run: |
mkdir -p artifacts
cp testresults*.xml artifacts/ 2>/dev/null || true
find .build -path "*/codecov/*.json" | xargs -I{} cp {} artifacts/ 2>/dev/null || true

- name: Upload test results
uses: actions/upload-artifact@v7
if: always()
with:
name: test-results-${{ matrix.os }}-${{ matrix.traits }}
path: artifacts/

15 changes: 15 additions & 0 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 7 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:6.1
// swift-tools-version:6.2

import PackageDescription

Expand All @@ -20,11 +20,15 @@ let package = Package(
traits: [
"Foundation"
],
dependencies: [],
dependencies: [
.package(url: "https://github.com/apple/swift-system", from: "1.6.1"),
],
targets: [
.target(
name: "CSErrors",
dependencies: []
dependencies: [
.product(name: "SystemPackage", package: "swift-system", condition: .when(platforms: [.linux]))
]
),
.testTarget(
name: "CSErrorsTests",
Expand Down
38 changes: 34 additions & 4 deletions Sources/CSErrors/CocoaError+CSErrors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
// Created by Charles Srstka on 4/17/20.
//

import System

#if canImport(Darwin)
import Darwin
#elseif canImport(Glibc)
import Glibc
#endif

#if Foundation
#if canImport(FoundationEssentials)
Expand All @@ -14,6 +19,12 @@ import FoundationEssentials
import Foundation
#endif

#if canImport(SystemPackage)
import SystemPackage
#else
import System
#endif

internal func cocoaCode(posixCode: Int32, isWrite: Bool) -> CocoaError.Code? {
switch posixCode {
case EPERM, EACCES:
Expand All @@ -28,8 +39,10 @@ internal func cocoaCode(posixCode: Int32, isWrite: Bool) -> CocoaError.Code? {
return .fileWriteOutOfSpace
case EROFS:
return .fileWriteVolumeReadOnly
#if canImport(Darwin)
case EFTYPE:
return .fileReadCorruptFile
#endif
case ECANCELED:
return .userCancelled
default:
Expand Down Expand Up @@ -94,7 +107,15 @@ extension CocoaError {
var userInfo = metadata.toUserInfo()

if let stringEncoding {
userInfo[NSStringEncodingErrorKey] = stringEncoding.rawValue
// The Darwin version of Foundation stores this as a UInt, other platforms store it as an NSNumber
// Also, the string keys they use are different.
// https://github.com/swiftlang/swift-foundation/blob/b011018acca72a38179bd4ac9d0377d2f90b4cff/Sources/FoundationEssentials/Error/CocoaError.swift#L108-L114

#if Foundation && canImport(Darwin)
userInfo[NSStringEncodingErrorKey] = NSNumber(value: stringEncoding.rawValue)
#endif

userInfo[NSStringEncodingErrorKeyNonDarwin] = Int(stringEncoding.rawValue)
}

self.init(code, userInfo: userInfo)
Expand Down Expand Up @@ -159,11 +180,20 @@ extension CocoaError {

extension CocoaError: CSErrorProtocol {
public var isFileNotFoundError: Bool {
[.fileNoSuchFile, .fileReadNoSuchFile, .ubiquitousFileUnavailable].contains(self.code)
switch self.code {
case .fileNoSuchFile, .fileReadNoSuchFile: true
#if canImport(Darwin)
case .ubiquitousFileUnavailable: true
#endif
default: false
}
}

public var isPermissionError: Bool {
[.fileReadNoPermission, .fileWriteNoPermission].contains(self.code)
switch self.code {
case .fileReadNoPermission, .fileWriteNoPermission: true
default: false
}
}

public var isCancelledError: Bool {
Expand Down
22 changes: 10 additions & 12 deletions Sources/CSErrors/Error+CSErrors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
// Created by Charles Srstka on 12/27/15.
//

#if canImport(SystemPackage)
import SystemPackage
#else
import System
#endif

#if canImport(Darwin)
import Darwin
Expand All @@ -25,25 +29,23 @@ extension Error {
/// True if the error represents a “File Not Found” error, regardless of domain.
public var isFileNotFoundError: Bool {
if #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, macCatalyst 14.0, *), versionCheck(11),
let errno = self as? System.Errno, errno == .noSuchFileOrDirectory {
let errno = self as? Errno, errno == .noSuchFileOrDirectory {
return true
}

if let err = self.toErrno(), err == ENOENT {
return true
}

#if canImport(Darwin)
if let osStatus = self.toOSStatus(), OSStatusError.Codes.fileNotFoundErrors.contains(osStatus) {
return true
}
#endif

if let err = self as? any CSErrorProtocol, err.isFileNotFoundError {
return true
}

#if Foundation
#if Foundation && canImport(Darwin)
if let err = (self as NSError).toCSErrorProtocol(), err.isFileNotFoundError {
return true
}
Expand All @@ -55,7 +57,7 @@ extension Error {
/// True if the error represents a permission or access error, regardless of domain.
public var isPermissionError: Bool {
if #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, macCatalyst 14.0, *), versionCheck(11),
let errno = self as? System.Errno, [.permissionDenied, .notPermitted].contains(errno)
let errno = self as? Errno, [.permissionDenied, .notPermitted].contains(errno)
{
return true
}
Expand All @@ -64,17 +66,15 @@ extension Error {
return true
}

#if canImport(Darwin)
if let osStatus = self.toOSStatus(), OSStatusError.Codes.permissionErrors.contains(osStatus) {
return true
}
#endif

if let err = self as? any CSErrorProtocol, err.isPermissionError {
return true
}

#if Foundation
#if Foundation && canImport(Darwin)
if let err = (self as NSError).toCSErrorProtocol(), err.isPermissionError {
return true
}
Expand All @@ -86,7 +86,7 @@ extension Error {
/// True if the error results from a user cancellation, regardless of domain.
public var isCancelledError: Bool {
if #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, macCatalyst 14.0, *), versionCheck(11),
let errno = self as? System.Errno, errno == .canceled
let errno = self as? Errno, errno == .canceled
{
return true
}
Expand All @@ -95,17 +95,15 @@ extension Error {
return true
}

#if canImport(Darwin)
if let err = self.toOSStatus(), OSStatusError.Codes.cancelErrors.contains(err) {
return true
}
#endif

if let err = self as? any CSErrorProtocol, err.isCancelledError {
return true
}

#if Foundation
#if Foundation && canImport(Darwin)
if let err = (self as NSError).toCSErrorProtocol(), err.isCancelledError {
return true
}
Expand Down
40 changes: 37 additions & 3 deletions Sources/CSErrors/ErrorMetadata.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,32 @@
// Created by Charles Srstka on 1/11/23.
//

#if canImport(SystemPackage)
import SystemPackage
#else
import System
#endif

#if Foundation
#if canImport(FoundationEssentials)
import FoundationEssentials
let NSFilePathErrorKey = "NSFilePath"
let NSHelpAnchorErrorKey = "NSHelpAnchor"
let NSLocalizedDescriptionKey = "NSLocalizedDescription"
let NSLocalizedFailureReasonErrorKey = "NSLocalizedFailureReason"
let NSRecoveryAttempterErrorKey = "NSRecoveryAttempter"
let NSLocalizedRecoveryOptionsErrorKey = "NSLocalizedRecoveryOptions"
let NSLocalizedRecoverySuggestionErrorKey = "NSLocalizedRecoverySuggestion"
let NSStringEncodingErrorKey = "NSStringEncoding"
let NSUnderlyingErrorKey = "NSUnderlyingError"
let NSURLErrorKey = "NSURL"
#else
import Foundation
#endif
#endif

let NSStringEncodingErrorKeyNonDarwin = "NSStringEncodingErrorKey"

public struct ErrorMetadata: Sendable {
public let description: String?
public internal(set) var failureReason: String?
Expand Down Expand Up @@ -159,7 +175,15 @@ public struct ErrorMetadata: Sendable {
}

if let stringEncoding {
custom[NSStringEncodingErrorKey] = stringEncoding.rawValue
// The Darwin version of Foundation stores this as a UInt, other platforms store it as an NSNumber
// Also, the string keys they use are different.
// https://github.com/swiftlang/swift-foundation/blob/b011018acca72a38179bd4ac9d0377d2f90b4cff/Sources/FoundationEssentials/Error/CocoaError.swift#L108-L114

#if Foundation && canImport(Darwin)
custom[NSStringEncodingErrorKey] = NSNumber(value: stringEncoding.rawValue)
#endif

custom[NSStringEncodingErrorKeyNonDarwin] = Int(stringEncoding.rawValue)
}

self.init(
Expand Down Expand Up @@ -233,20 +257,30 @@ public struct ErrorMetadata: Sendable {
return url
}

#if canImport(Darwin)
if #available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, macCatalyst 16.1, *), versionCheck(13),
let path = self.path, let url = URL(filePath: path) {
return url
} else if let path = self.pathString {
return URL(fileURLWithPath: path)
}
#else
if let path = self.path {
return URL(filePath: path.string)
}
#endif

return nil
}

public var stringEncoding: String.Encoding? {
guard let rawEncoding = self.custom?[NSStringEncodingErrorKey] as? UInt else { return nil }
if let rawEncoding = self.custom?[NSStringEncodingErrorKey] as? UInt {
return String.Encoding(rawValue: rawEncoding)
} else if let rawEncoding = self.custom?[NSStringEncodingErrorKeyNonDarwin] as? Int {
return String.Encoding(rawValue: UInt(rawEncoding))
}

return String.Encoding(rawValue: rawEncoding)
return nil
}
#endif
}
2 changes: 1 addition & 1 deletion Sources/CSErrors/HTTPError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public struct HTTPError: CSErrorProtocol {
public var failureReason: String? {
var reason = "HTTP \(self.statusCode)"

#if Foundation
#if Foundation && canImport(Darwin)
reason += " (\(HTTPURLResponse.localizedString(forStatusCode: self.statusCode)))"
#endif

Expand Down
5 changes: 3 additions & 2 deletions Sources/CSErrors/NSError+CSErrors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@
// Created by Charles Srstka on 11/11/23.
//

import System
#if Foundation && canImport(Darwin)

#if Foundation
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif

import System

extension NSError {
internal func toCSErrorProtocol() -> (any CSErrorProtocol)? {
switch self.domain {
Expand Down
Loading
Loading