Fix security-scoped URL access balancing
This commit is contained in:
@@ -24,9 +24,9 @@ public struct HTTPProxyCard: View {
|
||||
Spacer()
|
||||
Toggle("", isOn: $systemProxyEnabled)
|
||||
.labelsHidden()
|
||||
#if os(macOS)
|
||||
.toggleStyle(.switch)
|
||||
#endif
|
||||
#if os(macOS)
|
||||
.toggleStyle(.switch)
|
||||
#endif
|
||||
.onChangeCompat(of: systemProxyEnabled) { newValue in
|
||||
Task {
|
||||
await onToggle(newValue)
|
||||
|
||||
@@ -201,10 +201,11 @@ public struct NewProfileMenuView: View {
|
||||
let fileName = url.deletingPathExtension().lastPathComponent
|
||||
localImportRequest = NewProfileView.LocalImportRequest(name: fileName, fileURL: url)
|
||||
} else {
|
||||
_ = url.startAccessingSecurityScopedResource()
|
||||
defer { url.stopAccessingSecurityScopedResource() }
|
||||
|
||||
let content = try LibboxProfileContent.from(Data(contentsOf: url))
|
||||
let content = try url.withRequiredSecurityScopedAccess(
|
||||
or: NSError(domain: "NewProfileMenuView", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Missing access to selected file")])
|
||||
) {
|
||||
try LibboxProfileContent.from(Data(contentsOf: url))
|
||||
}
|
||||
|
||||
alert = AlertState(
|
||||
title: String(localized: "Import Profile"),
|
||||
|
||||
@@ -113,13 +113,11 @@ public final class NewProfileViewModel: BaseViewModel {
|
||||
guard let fileURL else {
|
||||
throw NSError(domain: "NewProfileViewModel", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Missing file")])
|
||||
}
|
||||
if !fileURL.startAccessingSecurityScopedResource() {
|
||||
throw NSError(domain: "NewProfileViewModel", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Missing access to selected file")])
|
||||
try fileURL.withRequiredSecurityScopedAccess(
|
||||
or: NSError(domain: "NewProfileViewModel", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Missing access to selected file")])
|
||||
) {
|
||||
try String(contentsOf: fileURL).write(to: profileConfig, atomically: true, encoding: .utf8)
|
||||
}
|
||||
defer {
|
||||
fileURL.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
try String(contentsOf: fileURL).write(to: profileConfig, atomically: true, encoding: .utf8)
|
||||
} else {
|
||||
try "{}".write(to: profileConfig, atomically: true, encoding: .utf8)
|
||||
}
|
||||
@@ -129,10 +127,6 @@ public final class NewProfileViewModel: BaseViewModel {
|
||||
try FileManager.default.createDirectory(at: FilePath.iCloudDirectory, withIntermediateDirectories: true)
|
||||
}
|
||||
let saveURL = FilePath.iCloudDirectory.appendingPathComponent(remotePath, isDirectory: false)
|
||||
_ = saveURL.startAccessingSecurityScopedResource()
|
||||
defer {
|
||||
saveURL.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
do {
|
||||
_ = try String(contentsOf: saveURL)
|
||||
} catch {
|
||||
|
||||
@@ -7,10 +7,6 @@ public extension Profile {
|
||||
return try String(contentsOfFile: path)
|
||||
case .icloud:
|
||||
let saveURL = FilePath.iCloudDirectory.appendingPathComponent(path)
|
||||
_ = saveURL.startAccessingSecurityScopedResource()
|
||||
defer {
|
||||
saveURL.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
return try String(contentsOf: saveURL)
|
||||
}
|
||||
}
|
||||
@@ -21,10 +17,6 @@ public extension Profile {
|
||||
try content.write(toFile: path, atomically: true, encoding: .utf8)
|
||||
case .icloud:
|
||||
let saveURL = FilePath.iCloudDirectory.appendingPathComponent(path)
|
||||
_ = saveURL.startAccessingSecurityScopedResource()
|
||||
defer {
|
||||
saveURL.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
try content.write(to: saveURL, atomically: true, encoding: .utf8)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Combine
|
||||
import Foundation
|
||||
import Libbox
|
||||
import os
|
||||
import Combine
|
||||
|
||||
private let logger = Logger(category: "CommandClient")
|
||||
|
||||
@@ -83,6 +83,7 @@ public class CommandClient: ObservableObject {
|
||||
.map(\.status)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
@Published public var groups: [LibboxOutboundGroup]?
|
||||
@Published public var logList: [LogEntry]
|
||||
@Published public var defaultLogLevel = 0
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import Foundation
|
||||
|
||||
public extension URL {
|
||||
/// Best-effort security-scoped access:
|
||||
/// - If `startAccessingSecurityScopedResource()` succeeds, access is relinquished via `stop...` when `body` completes.
|
||||
/// - If it fails, `body` still runs (useful for non-security-scoped but otherwise accessible URLs).
|
||||
@discardableResult
|
||||
func withSecurityScopedAccess<T>(_ body: () throws -> T) rethrows -> T {
|
||||
let didStart = startAccessingSecurityScopedResource()
|
||||
defer {
|
||||
if didStart {
|
||||
stopAccessingSecurityScopedResource()
|
||||
}
|
||||
}
|
||||
return try body()
|
||||
}
|
||||
|
||||
/// Async best-effort variant of `withSecurityScopedAccess`.
|
||||
@discardableResult
|
||||
func withSecurityScopedAccess<T>(_ body: () async throws -> T) async rethrows -> T {
|
||||
let didStart = startAccessingSecurityScopedResource()
|
||||
defer {
|
||||
if didStart {
|
||||
stopAccessingSecurityScopedResource()
|
||||
}
|
||||
}
|
||||
return try await body()
|
||||
}
|
||||
|
||||
/// Required security-scoped access:
|
||||
/// - If `startAccessingSecurityScopedResource()` fails, throws `error` and does not run `body`.
|
||||
/// - Otherwise, guarantees a balanced `stop...` when `body` completes.
|
||||
@discardableResult
|
||||
func withRequiredSecurityScopedAccess<T>(
|
||||
or error: @autoclosure () -> any Error,
|
||||
_ body: () throws -> T
|
||||
) throws -> T {
|
||||
guard startAccessingSecurityScopedResource() else {
|
||||
throw error()
|
||||
}
|
||||
defer { stopAccessingSecurityScopedResource() }
|
||||
return try body()
|
||||
}
|
||||
|
||||
/// Async required variant of `withRequiredSecurityScopedAccess`.
|
||||
@discardableResult
|
||||
func withRequiredSecurityScopedAccess<T>(
|
||||
or error: @autoclosure () -> any Error,
|
||||
_ body: () async throws -> T
|
||||
) async throws -> T {
|
||||
guard startAccessingSecurityScopedResource() else {
|
||||
throw error()
|
||||
}
|
||||
defer { stopAccessingSecurityScopedResource() }
|
||||
return try await body()
|
||||
}
|
||||
}
|
||||
@@ -58,9 +58,9 @@ public class MainViewModel: BaseViewModel {
|
||||
|
||||
private func importURLProfile(_ url: URL) async {
|
||||
do {
|
||||
_ = url.startAccessingSecurityScopedResource()
|
||||
importProfile = try await .from(readURL(url))
|
||||
url.stopAccessingSecurityScopedResource()
|
||||
importProfile = try await url.withSecurityScopedAccess {
|
||||
try await .from(readURL(url))
|
||||
}
|
||||
} catch {
|
||||
alert = AlertState(error: error)
|
||||
}
|
||||
|
||||
+3
-3
@@ -222,9 +222,9 @@ struct MainView: View {
|
||||
}
|
||||
} else if url.pathExtension == "bpf" {
|
||||
do {
|
||||
_ = url.startAccessingSecurityScopedResource()
|
||||
importProfile = try .from(Data(contentsOf: url))
|
||||
url.stopAccessingSecurityScopedResource()
|
||||
importProfile = try url.withSecurityScopedAccess {
|
||||
try .from(Data(contentsOf: url))
|
||||
}
|
||||
} catch {
|
||||
alert = AlertState(error: error)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user