2525import CryptoKit
2626import Foundation
2727
28+ /// A named formatting profile pairing a SwiftFormat configuration URL with an
29+ /// uncrustify configuration URL.
30+ ///
31+ /// Configurations are persisted via ``Preferences`` and synced between the
32+ /// main app and the editor extension. Each remote configuration file is
33+ /// downloaded over HTTPS into the shared app-group container, keyed by a hash
34+ /// of its URL and stored alongside a content hash so a tampered or partial
35+ /// cache can be rejected at read time.
2836@objc
2937public class Configuration : NSObject , Codable
3038{
39+ /// User-visible name of the configuration.
3140 @objc public dynamic var name : String
41+
42+ /// URL of the SwiftFormat configuration file, or `nil` if none.
3243 @objc public dynamic var swiftFormat : URL ?
44+
45+ /// URL of the uncrustify configuration file, or `nil` if none.
3346 @objc public dynamic var uncrustify : URL ?
47+
48+ /// Whether a download of this configuration's files is in progress.
49+ /// KVO-observable so UI can show progress.
3450 @objc public dynamic var downloading = false
3551
52+ /// The built-in configurations seeded on first launch.
3653 public static var defaultConfigurations : [ Configuration ]
3754 {
3855 [
@@ -43,6 +60,13 @@ public class Configuration: NSObject, Codable
4360 ]
4461 }
4562
63+ /// Creates a configuration from a name and an optional URL for each
64+ /// formatter.
65+ ///
66+ /// - Parameters:
67+ /// - name: User-visible name.
68+ /// - swiftFormat: SwiftFormat configuration URL, or `nil`.
69+ /// - uncrustify: Uncrustify configuration URL, or `nil`.
4670 public init ( name: String , swiftFormat: URL ? , uncrustify: URL ? )
4771 {
4872 self . name = name
@@ -52,11 +76,17 @@ public class Configuration: NSObject, Codable
5276 super. init ( )
5377 }
5478
79+ /// A textual description including the configuration's name, for debugging.
5580 public override var description : String
5681 {
5782 " \( super. description ) : \( self . name ) "
5883 }
5984
85+ /// Compares two configurations by value.
86+ ///
87+ /// - Parameter object: The object to compare against.
88+ /// - Returns: `true` when `object` is a `Configuration` with the same name
89+ /// and formatter URLs.
6090 public override func isEqual( _ object: Any ? ) -> Bool
6191 {
6292 guard let configuration = object as? Configuration
@@ -75,6 +105,8 @@ public class Configuration: NSObject, Codable
75105 return false
76106 }
77107
108+ /// A hash derived from the name and formatter URLs, consistent with
109+ /// ``isEqual(_:)``.
78110 public override var hash : Int
79111 {
80112 var hasher = Hasher ( )
@@ -86,6 +118,11 @@ public class Configuration: NSObject, Codable
86118 return hasher. finalize ( )
87119 }
88120
121+ /// Downloads this configuration's formatter files into the shared cache.
122+ ///
123+ /// Coalesces concurrent calls via the ``downloading`` flag (checked and set
124+ /// on the main queue), then fetches each non-`nil` URL on a background
125+ /// queue, clearing the flag when finished.
89126 public func download( )
90127 {
91128 DispatchQueue . main. async
@@ -117,6 +154,15 @@ public class Configuration: NSObject, Codable
117154 }
118155 }
119156
157+ /// Downloads a single configuration file and writes it to the shared cache.
158+ ///
159+ /// Rejects non-HTTPS URLs, names the cached file by the hash of its URL,
160+ /// and writes a `<hash>.sha256` sidecar holding the content hash so the
161+ /// cache can be integrity-checked on read. File writes are serialized
162+ /// through an `NSFileCoordinator`. Any failure is silently ignored, leaving
163+ /// the cache untouched.
164+ ///
165+ /// - Parameter url: HTTPS URL of the configuration file to fetch.
120166 private func download( url: URL )
121167 {
122168 guard url. scheme? . lowercased ( ) == " https " ,
@@ -154,6 +200,10 @@ public class Configuration: NSObject, Codable
154200 /// Fetches a configuration over HTTPS with an explicit timeout, returning
155201 /// the body only for a 2xx response. Runs synchronously; intended to be
156202 /// called from a background queue.
203+ ///
204+ /// - Parameter url: URL of the configuration file to fetch.
205+ /// - Returns: The response body for a 2xx response, or `nil` on a transport
206+ /// error or non-2xx status.
157207 private static func fetch( url: URL ) -> Data ?
158208 {
159209 var request = URLRequest ( url: url, timeoutInterval: 30 )
@@ -189,6 +239,23 @@ public class Configuration: NSObject, Codable
189239 return result
190240 }
191241
242+ /// Provides local copies of the cached configuration files to a closure,
243+ /// then lets the caller clean them up.
244+ ///
245+ /// Each available file is copied to a unique temporary location (its
246+ /// integrity verified against the stored content hash). If a configured URL
247+ /// has no valid cached copy, a fresh ``download()`` is triggered for next
248+ /// time. When neither file is available the `error` closure is called and
249+ /// `completion` is not. Otherwise `completion` receives the temporary URLs
250+ /// plus a `finished` closure that deletes them; the caller must invoke
251+ /// `finished` once done.
252+ ///
253+ /// - Parameters:
254+ /// - completion: Called with the temporary `swiftFormat` / `uncrustify`
255+ /// URLs (either may be `nil`) and a `finished` cleanup
256+ /// closure.
257+ /// - error: Called instead of `completion` when no cached file is
258+ /// available.
192259 public func withConfigurations( completion: ( ( swiftFormat: URL ? , uncrustify: URL ? , finished: ( ) -> Void ) ) -> Void , error: ( ) -> Void )
193260 {
194261 let swiftFormat = self . copy ( url: self . swiftFormat )
@@ -225,6 +292,19 @@ public class Configuration: NSObject, Codable
225292 completion ( ( swiftFormat: swiftFormat, uncrustify: uncrustify, finished: finished ) )
226293 }
227294
295+ /// Copies a cached configuration file to a unique temporary URL after
296+ /// verifying its integrity.
297+ ///
298+ /// Locates the cached file by the hash of `url`, and — when a `<hash>.sha256`
299+ /// sidecar exists — rejects the copy if the bytes no longer match, so a
300+ /// tampered or partially written cache is never fed to the formatter. File
301+ /// access is serialized through an `NSFileCoordinator`.
302+ ///
303+ /// - Parameter url: The original configuration URL whose cached copy is
304+ /// wanted, or `nil`.
305+ /// - Returns: A temporary URL holding a fresh copy of the cached file, or
306+ /// `nil` if `url` is `nil`, nothing is cached, or the integrity
307+ /// check or copy fails.
228308 private func copy( url: URL ? ) -> URL ?
229309 {
230310 guard let url = url,
0 commit comments