Skip to content
Closed
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
6 changes: 3 additions & 3 deletions .github/workflows/macOS.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ jobs:
steps:
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: 16.0
xcode-version: 16.2
- name: Set up Swift
uses: swift-actions/setup-swift@v2
with:
swift-version: '6.1.0'
with:
swift-version: '6.2'
- uses: actions/checkout@v3
- name: Build
run: swift build -v
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ jobs:
steps:
- name: Set up Swift
uses: swift-actions/setup-swift@v2
with:
swift-version: '6.1.0'
with:
swift-version: '6.2'
- uses: actions/checkout@v3
- name: Build for release
run: swift build -v -c release
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ jobs:
- uses: actions/checkout@v4

# ① Install Swift for Windows
- name: Set up Swift 6.1
- name: Set up Swift 6.2
uses: compnerd/gha-setup-swift@main
with:
branch: swift-6.1-release # release branch
tag: 6.1-RELEASE # exact toolchain tag
branch: swift-6.2-release # release branch
tag: 6.2-RELEASE # exact toolchain tag

# ② Build & test
- run: swift --version # sanity-check
Expand Down
22 changes: 15 additions & 7 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import PackageDescription
let package = Package(
name: "AppState",
platforms: [
.iOS(.v15),
.watchOS(.v8),
.macOS(.v11),
.tvOS(.v15),
.visionOS(.v1)
.iOS(.v18),
.watchOS(.v11),
.macOS(.v15),
.tvOS(.v18),
.visionOS(.v2)
],
products: [
.library(
Expand All @@ -18,19 +18,27 @@ let package = Package(
)
],
dependencies: [
.package(url: "https://github.com/0xLeif/Cache", from: "2.0.0"),
.package(url: "https://github.com/0xLeif/Cache", branch: "leif/mutex"),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using a git branch for a dependency is generally discouraged for shared or production code as it can lead to build instability. Branches can be force-pushed, rebased, or deleted, which would break builds for anyone using this package. For better stability and reproducible builds, it's recommended to depend on a specific version tag or a commit hash. Please consider updating this to a stable release version once the leif/mutex branch is merged and a new version of Cache is tagged.

Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Cache dependency is pinned to a mutable Git branch (branch: "leif/mutex"), which creates a supply chain risk because future builds can silently pull different, attacker-controlled code without any change to this repository. An attacker who compromises the upstream repo or branch could inject malicious logic into your library at build time. To mitigate this, pin the dependency to an immutable reference such as a released semantic version or a specific commit SHA and update it explicitly when needed.

Copilot uses AI. Check for mistakes.
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.4.0")
],
targets: [
.target(
name: "AppState",
dependencies: [
"Cache"
],
swiftSettings: [
.swiftLanguageMode(.v6),
.enableExperimentalFeature("StrictConcurrency")
]
),
.testTarget(
name: "AppStateTests",
dependencies: ["AppState"]
dependencies: ["AppState"],
swiftSettings: [
.swiftLanguageMode(.v6),
.enableExperimentalFeature("StrictConcurrency")
]
)
]
)
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ Read this in other languages: [French](documentation/README.fr.md) | [German](do

## Requirements

- **iOS**: 15.0+
- **watchOS**: 8.0+
- **macOS**: 11.0+
- **tvOS**: 15.0+
- **visionOS**: 1.0+
- **Swift**: 6.0+
- **Xcode**: 16.0+
- **iOS**: 18.0+
- **watchOS**: 11.0+
- **macOS**: 15.0+
- **tvOS**: 18.0+
- **visionOS**: 2.0+
- **Swift**: 6.2+
- **Xcode**: 16.2+

**Non-Apple Platform Support**: Linux & Windows

Expand Down
6 changes: 2 additions & 4 deletions Sources/AppState/Application/Application+internal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,11 @@ extension Application {
)
}

/// Returns value for the provided keyPath. This method is thread safe
/// Returns value for the provided keyPath. Thread safety is provided by Cache's internal Mutex.
///
/// - Parameter keyPath: KeyPath of the value to be fetched
func value<Value>(keyPath: KeyPath<Application, Value>) -> Value {
lock.lock(); defer { lock.unlock() }

return self[keyPath: keyPath]
self[keyPath: keyPath]
}

/**
Expand Down
4 changes: 0 additions & 4 deletions Sources/AppState/Application/Application.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,6 @@ open class Application: NSObject {
@MainActor
static var isLoggingEnabled: Bool = false

/// A recursive lock to ensure thread-safe access to shared resources within the Application instance.
let lock: NSRecursiveLock

/// The underlying cache used to store all state and dependency values.
let cache: Cache<String, Any>

Expand All @@ -51,7 +48,6 @@ open class Application: NSObject {
public required init(
setup: (Application) -> Void = { _ in }
) {
lock = NSRecursiveLock()
cache = Cache()

super.init()
Expand Down
45 changes: 15 additions & 30 deletions Sources/AppState/Dependencies/Keychain.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#if !os(Linux) && !os(Windows)
import Cache
import Foundation
import Synchronization

/**
A `Keychain` class that adopts the `Cacheable` protocol.
Expand All @@ -25,23 +26,19 @@ public final class Keychain: Sendable {
public typealias Key = String
public typealias Value = String

private let lock: NSLock
@MainActor
private var keys: Set<Key>

private let keys: Mutex<Set<Key>>

/// Default initializer
public init() {
self.lock = NSLock()
self.keys = []
self.keys = Mutex([])
}

/**
Initialize with a predefined set of keys.
- Parameter keys: The predefined set of keys.
*/
public init(keys: Set<Key>) {
self.lock = NSLock()
self.keys = keys
self.keys = Mutex(keys)
}

/**
Expand All @@ -57,19 +54,16 @@ public final class Keychain: Sendable {
kSecReturnData: true,
kSecMatchLimit: kSecMatchLimitOne
]

var dataTypeRef: AnyObject?

lock.lock()
let status: OSStatus = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
lock.unlock()


guard
status == noErr,
let data = dataTypeRef as? Data,
let output = String(data: data, encoding: .utf8) as? Output
else { return nil }

return output
}

Expand Down Expand Up @@ -113,9 +107,7 @@ public final class Keychain: Sendable {
SecItemAdd(addQuery as CFDictionary, nil)
}

Task { @MainActor in
keys.insert(key)
}
_ = keys.withLock { $0.insert(key) }
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The key is added to the internal keys set regardless of whether the keychain operation succeeds. If SecItemAdd fails for reasons other than the item already existing (e.g., keychain access denied, disk full), the key will still be tracked in the set even though the value isn't actually stored in the keychain. This could lead to inconsistencies between the tracked keys and actual keychain contents. Consider only adding the key to the set if the keychain operation succeeds (status == errSecSuccess).

Copilot uses AI. Check for mistakes.
}

/**
Expand All @@ -127,10 +119,9 @@ public final class Keychain: Sendable {
kSecClass: kSecClassGenericPassword,
kSecAttrAccount: key
]

lock.lock()

SecItemDelete(query as CFDictionary)
lock.unlock()
_ = keys.withLock { $0.remove(key) }
Comment on lines 123 to +124
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The key is removed from the internal keys set regardless of whether SecItemDelete succeeds. If the deletion fails (e.g., item doesn't exist in keychain, access denied), the key will still be removed from the tracked set, causing the internal state to be inconsistent with the actual keychain contents. Consider only removing the key from the set if SecItemDelete succeeds (status == errSecSuccess) or if the item doesn't exist (status == errSecItemNotFound).

Copilot uses AI. Check for mistakes.
}

/**
Expand Down Expand Up @@ -174,21 +165,16 @@ public final class Keychain: Sendable {
- Parameter ofType: The type of the values expected.
- Returns: A dictionary with keys and their associated values.
*/
@MainActor
public func values<Output>(ofType: Output.Type) -> [Key: Output] {
let storedKeys: [Key]
let storedKeys = keys.withLock { Array($0) }
var values: [Key: Output] = [:]

lock.lock()
storedKeys = Array(keys)
lock.unlock()


for key in storedKeys {
if let value = get(key, as: Output.self) {
values[key] = value
}
}

return values
}
}
Expand Down Expand Up @@ -217,7 +203,6 @@ public extension Keychain {
Returns all keys and their string values currently in the keychain.
- Returns: A dictionary with keys and their corresponding string values.
*/
@MainActor
func values() -> [Key: String] {
values(ofType: String.self)
}
Expand Down
Loading
Loading