PersistedProperty.swift 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. import Foundation
  2. /// Attention! Do not use this wrapper for mutating structure with `didSet` handler into property owner!
  3. /// `didSet` will never called if structure mutate into itself (by "mutating functions").
  4. @propertyWrapper struct Persisted<Value: Codable & Equatable> {
  5. var wrappedValue: Value {
  6. get { getValue() ?? initialValue }
  7. set { setValue(newValue) }
  8. }
  9. private func getValue() -> Value? {
  10. lock?.lock()
  11. defer { lock?.unlock() }
  12. return storage.getValue(Value.self, forKey: key)
  13. }
  14. private mutating func setValue(_ value: Value) {
  15. lock?.lock()
  16. defer { lock?.unlock() }
  17. storage.setValue(value, forKey: key)
  18. }
  19. private let key: String
  20. private let storage: KeyValueStorage
  21. private let lock: NSRecursiveLock?
  22. private let initialValue: Value
  23. var isInitialValue: Bool {
  24. if let value = getValue() {
  25. return value == initialValue
  26. }
  27. return true
  28. }
  29. init(
  30. wrappedValue: Value,
  31. key: String,
  32. storage: KeyValueStorage = UserDefaults.standard,
  33. lock: NSRecursiveLock? = nil
  34. ) {
  35. self.storage = storage
  36. self.key = key
  37. self.lock = lock
  38. initialValue = wrappedValue
  39. lock?.lock()
  40. defer { lock?.unlock() }
  41. if storage.getValue(Value.self, forKey: key) == nil {
  42. setValue(wrappedValue)
  43. }
  44. }
  45. }
  46. @propertyWrapper public struct PersistedProperty<Value> {
  47. let key: String
  48. let storageURL: URL
  49. public init(key: String) {
  50. self.key = key
  51. let documents: URL
  52. guard let localDocuments = try? FileManager.default.url(
  53. for: .documentDirectory,
  54. in: .userDomainMask,
  55. appropriateFor: nil,
  56. create: true
  57. ) else {
  58. preconditionFailure("Could not get a documents directory URL.")
  59. }
  60. documents = localDocuments
  61. storageURL = documents.appendingPathComponent(key + ".plist")
  62. }
  63. public var wrappedValue: Value? {
  64. get {
  65. do {
  66. let data = try Data(contentsOf: storageURL)
  67. guard let value = try PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? Value
  68. else {
  69. debug(.storage, "[PersistedProperty:\(key)] Could not cast property list to expected type.")
  70. return nil
  71. }
  72. return value
  73. } catch {
  74. debug(.storage, "❌ [PersistedProperty:\(key)] Failed to read value: \(error)")
  75. return nil
  76. }
  77. }
  78. set {
  79. guard let newValue = newValue else {
  80. do {
  81. try FileManager.default.removeItem(at: storageURL)
  82. debug(.storage, "[PersistedProperty:\(key)] Removed value.")
  83. } catch {
  84. debug(.storage, "❌ [PersistedProperty:\(key)] Failed to remove value: \(error)")
  85. }
  86. return
  87. }
  88. do {
  89. let data = try PropertyListSerialization.data(fromPropertyList: newValue, format: .binary, options: 0)
  90. try data.write(to: storageURL, options: .atomic)
  91. // Ensure appropriate protection level
  92. try FileManager.default.setAttributes(
  93. [.protectionKey: FileProtectionType.none],
  94. ofItemAtPath: storageURL.path
  95. )
  96. debug(.storage, "✅ [PersistedProperty:\(key)] Saved value successfully.")
  97. } catch {
  98. debug(.storage, "❌ [PersistedProperty:\(key)] Failed to write value: \(error)")
  99. }
  100. }
  101. }
  102. }
  103. import Foundation
  104. enum FileProtectionFixer {
  105. /// Ensures only critical persisted flags have safe file protection set.
  106. static func fixFlagFileProtectionForPropertyPersistentFlags() {
  107. let flagFiles = [
  108. "onboardingCompleted.plist",
  109. "diagnosticsSharing.plist",
  110. "lastCleanupDate.plist",
  111. "hasSeenFatProteinOrderChange.plist",
  112. "telemetryEnabled.plist",
  113. "telemetryConsentDecisionMade.plist",
  114. "telemetryLastSentAt.plist",
  115. "telemetryLastSentSha.plist",
  116. "telemetryColdLaunchTimes.plist",
  117. "telemetryInstallId.plist",
  118. "telemetryAttestForbidden.plist",
  119. "telemetryDebugServerURL.plist"
  120. ]
  121. let fileManager = FileManager.default
  122. guard let documentsURL = try? fileManager.url(
  123. for: .documentDirectory,
  124. in: .userDomainMask,
  125. appropriateFor: nil,
  126. create: false
  127. ) else {
  128. debug(.storage, "⚠️ Could not access the documents directory.")
  129. return
  130. }
  131. for fileName in flagFiles {
  132. let fileURL = documentsURL.appendingPathComponent(fileName)
  133. guard fileManager.fileExists(atPath: fileURL.path) else {
  134. continue // Skip if file doesn’t exist yet
  135. }
  136. do {
  137. try fileManager.setAttributes(
  138. [.protectionKey: FileProtectionType.none],
  139. ofItemAtPath: fileURL.path
  140. )
  141. debug(.storage, "✅ Updated protection for \(fileName)")
  142. } catch {
  143. debug(.storage, "❌ Failed to update protection for \(fileName): \(error)")
  144. }
  145. }
  146. }
  147. }