Fix security-scoped URL access balancing
This commit is contained in:
@@ -201,10 +201,11 @@ public struct NewProfileMenuView: View {
|
|||||||
let fileName = url.deletingPathExtension().lastPathComponent
|
let fileName = url.deletingPathExtension().lastPathComponent
|
||||||
localImportRequest = NewProfileView.LocalImportRequest(name: fileName, fileURL: url)
|
localImportRequest = NewProfileView.LocalImportRequest(name: fileName, fileURL: url)
|
||||||
} else {
|
} else {
|
||||||
_ = url.startAccessingSecurityScopedResource()
|
let content = try url.withRequiredSecurityScopedAccess(
|
||||||
defer { url.stopAccessingSecurityScopedResource() }
|
or: NSError(domain: "NewProfileMenuView", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Missing access to selected file")])
|
||||||
|
) {
|
||||||
let content = try LibboxProfileContent.from(Data(contentsOf: url))
|
try LibboxProfileContent.from(Data(contentsOf: url))
|
||||||
|
}
|
||||||
|
|
||||||
alert = AlertState(
|
alert = AlertState(
|
||||||
title: String(localized: "Import Profile"),
|
title: String(localized: "Import Profile"),
|
||||||
|
|||||||
@@ -113,13 +113,11 @@ public final class NewProfileViewModel: BaseViewModel {
|
|||||||
guard let fileURL else {
|
guard let fileURL else {
|
||||||
throw NSError(domain: "NewProfileViewModel", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Missing file")])
|
throw NSError(domain: "NewProfileViewModel", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Missing file")])
|
||||||
}
|
}
|
||||||
if !fileURL.startAccessingSecurityScopedResource() {
|
try fileURL.withRequiredSecurityScopedAccess(
|
||||||
throw NSError(domain: "NewProfileViewModel", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Missing access to selected file")])
|
or: NSError(domain: "NewProfileViewModel", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Missing access to selected file")])
|
||||||
}
|
) {
|
||||||
defer {
|
|
||||||
fileURL.stopAccessingSecurityScopedResource()
|
|
||||||
}
|
|
||||||
try String(contentsOf: fileURL).write(to: profileConfig, atomically: true, encoding: .utf8)
|
try String(contentsOf: fileURL).write(to: profileConfig, atomically: true, encoding: .utf8)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
try "{}".write(to: profileConfig, atomically: true, encoding: .utf8)
|
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)
|
try FileManager.default.createDirectory(at: FilePath.iCloudDirectory, withIntermediateDirectories: true)
|
||||||
}
|
}
|
||||||
let saveURL = FilePath.iCloudDirectory.appendingPathComponent(remotePath, isDirectory: false)
|
let saveURL = FilePath.iCloudDirectory.appendingPathComponent(remotePath, isDirectory: false)
|
||||||
_ = saveURL.startAccessingSecurityScopedResource()
|
|
||||||
defer {
|
|
||||||
saveURL.stopAccessingSecurityScopedResource()
|
|
||||||
}
|
|
||||||
do {
|
do {
|
||||||
_ = try String(contentsOf: saveURL)
|
_ = try String(contentsOf: saveURL)
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -7,10 +7,6 @@ public extension Profile {
|
|||||||
return try String(contentsOfFile: path)
|
return try String(contentsOfFile: path)
|
||||||
case .icloud:
|
case .icloud:
|
||||||
let saveURL = FilePath.iCloudDirectory.appendingPathComponent(path)
|
let saveURL = FilePath.iCloudDirectory.appendingPathComponent(path)
|
||||||
_ = saveURL.startAccessingSecurityScopedResource()
|
|
||||||
defer {
|
|
||||||
saveURL.stopAccessingSecurityScopedResource()
|
|
||||||
}
|
|
||||||
return try String(contentsOf: saveURL)
|
return try String(contentsOf: saveURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,10 +17,6 @@ public extension Profile {
|
|||||||
try content.write(toFile: path, atomically: true, encoding: .utf8)
|
try content.write(toFile: path, atomically: true, encoding: .utf8)
|
||||||
case .icloud:
|
case .icloud:
|
||||||
let saveURL = FilePath.iCloudDirectory.appendingPathComponent(path)
|
let saveURL = FilePath.iCloudDirectory.appendingPathComponent(path)
|
||||||
_ = saveURL.startAccessingSecurityScopedResource()
|
|
||||||
defer {
|
|
||||||
saveURL.stopAccessingSecurityScopedResource()
|
|
||||||
}
|
|
||||||
try content.write(to: saveURL, atomically: true, encoding: .utf8)
|
try content.write(to: saveURL, atomically: true, encoding: .utf8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
import Libbox
|
import Libbox
|
||||||
import os
|
import os
|
||||||
import Combine
|
|
||||||
|
|
||||||
private let logger = Logger(category: "CommandClient")
|
private let logger = Logger(category: "CommandClient")
|
||||||
|
|
||||||
@@ -83,6 +83,7 @@ public class CommandClient: ObservableObject {
|
|||||||
.map(\.status)
|
.map(\.status)
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Published public var groups: [LibboxOutboundGroup]?
|
@Published public var groups: [LibboxOutboundGroup]?
|
||||||
@Published public var logList: [LogEntry]
|
@Published public var logList: [LogEntry]
|
||||||
@Published public var defaultLogLevel = 0
|
@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 {
|
private func importURLProfile(_ url: URL) async {
|
||||||
do {
|
do {
|
||||||
_ = url.startAccessingSecurityScopedResource()
|
importProfile = try await url.withSecurityScopedAccess {
|
||||||
importProfile = try await .from(readURL(url))
|
try await .from(readURL(url))
|
||||||
url.stopAccessingSecurityScopedResource()
|
}
|
||||||
} catch {
|
} catch {
|
||||||
alert = AlertState(error: error)
|
alert = AlertState(error: error)
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -222,9 +222,9 @@ struct MainView: View {
|
|||||||
}
|
}
|
||||||
} else if url.pathExtension == "bpf" {
|
} else if url.pathExtension == "bpf" {
|
||||||
do {
|
do {
|
||||||
_ = url.startAccessingSecurityScopedResource()
|
importProfile = try url.withSecurityScopedAccess {
|
||||||
importProfile = try .from(Data(contentsOf: url))
|
try .from(Data(contentsOf: url))
|
||||||
url.stopAccessingSecurityScopedResource()
|
}
|
||||||
} catch {
|
} catch {
|
||||||
alert = AlertState(error: error)
|
alert = AlertState(error: error)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user