Fix localization bundle and attributed string builder crashes on unsynchronised access to their internal caches

This commit is contained in:
Stefan Ceriu
2024-02-15 09:17:36 +02:00
committed by Stefan Ceriu
parent 99d6babc30
commit e0c9f43026
2 changed files with 52 additions and 16 deletions
+24 -8
View File
@@ -32,13 +32,17 @@ public extension Bundle {
// MARK: - Localisation // MARK: - Localisation
private static var cachedLocalizationBundles = [String: Bundle]() /// Overrides `Bundle.app.preferredLocalizations` for testing translations.
static var overrideLocalizations: [String]?
private static let cacheDispatchQueue = DispatchQueue(label: "io.element.elementx.localization_bundle_cache")
private static var cachedBundles = [String: Bundle]()
/// Get an lproj language bundle from the receiver bundle. /// Get an lproj language bundle from the receiver bundle.
/// - Parameter language: The language to try to load. /// - Parameter language: The language to try to load.
/// - Returns: The lproj bundle if found otherwise nil. /// - Returns: The lproj bundle if found otherwise nil.
static func lprojBundle(for language: String) -> Bundle? { static func lprojBundle(for language: String) -> Bundle? {
if let bundle = cachedLocalizationBundles[language] { if let bundle = cachedValue(forKey: language) {
return bundle return bundle
} }
@@ -48,13 +52,25 @@ public extension Bundle {
let bundle = Bundle(url: lprojURL) let bundle = Bundle(url: lprojURL)
DispatchQueue.main.async { cacheValue(bundle, forKey: language)
cachedLocalizationBundles[language] = bundle
}
return bundle return bundle
} }
/// Overrides `Bundle.app.preferredLocalizations` for testing translations. // MARK: - Private
static var overrideLocalizations: [String]?
private static func cacheValue(_ value: Bundle?, forKey key: String) {
cacheDispatchQueue.sync {
cachedBundles[key] = value
}
}
private static func cachedValue(forKey key: String) -> Bundle? {
var result: Bundle?
cacheDispatchQueue.sync {
result = cachedBundles[key]
}
return result
}
} }
@@ -26,6 +26,8 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
private let mentionBuilder: MentionBuilderProtocol private let mentionBuilder: MentionBuilderProtocol
private static let defaultKey = "default" private static let defaultKey = "default"
private static let cacheDispatchQueue = DispatchQueue(label: "io.element.elementx.attributed_string_builder_cache")
private static var caches: [String: LRUCache<String, AttributedString>] = [:] private static var caches: [String: LRUCache<String, AttributedString>] = [:]
static func invalidateCaches() { static func invalidateCaches() {
@@ -34,9 +36,6 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
init(cacheKey: String = defaultKey, permalinkBaseURL: URL, mentionBuilder: MentionBuilderProtocol) { init(cacheKey: String = defaultKey, permalinkBaseURL: URL, mentionBuilder: MentionBuilderProtocol) {
self.cacheKey = cacheKey self.cacheKey = cacheKey
if Self.caches[cacheKey] == nil {
Self.caches[cacheKey] = LRUCache<String, AttributedString>(countLimit: 1000)
}
self.permalinkBaseURL = permalinkBaseURL self.permalinkBaseURL = permalinkBaseURL
self.mentionBuilder = mentionBuilder self.mentionBuilder = mentionBuilder
} }
@@ -46,7 +45,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
return nil return nil
} }
if let cached = Self.caches[cacheKey]?.value(forKey: string) { if let cached = Self.cachedValue(forKey: string, cacheKey: cacheKey) {
return cached return cached
} }
@@ -56,7 +55,8 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
removeLinkColors(mutableAttributedString) removeLinkColors(mutableAttributedString)
let result = try? AttributedString(mutableAttributedString, including: \.elementX) let result = try? AttributedString(mutableAttributedString, including: \.elementX)
Self.caches[cacheKey]?.setValue(result, forKey: string) Self.cacheValue(result, forKey: string, cacheKey: cacheKey)
return result return result
} }
@@ -72,10 +72,10 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
return nil return nil
} }
if let cached = Self.caches[cacheKey]?.value(forKey: originalHTMLString) { if let cached = Self.cachedValue(forKey: originalHTMLString, cacheKey: cacheKey) {
return cached return cached
} }
let htmlString = originalHTMLString.replacingHtmlBreaksOccurrences() let htmlString = originalHTMLString.replacingHtmlBreaksOccurrences()
guard let data = htmlString.data(using: .utf8) else { guard let data = htmlString.data(using: .utf8) else {
@@ -115,12 +115,32 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
removeDTCoreTextArtifacts(mutableAttributedString) removeDTCoreTextArtifacts(mutableAttributedString)
let result = try? AttributedString(mutableAttributedString, including: \.elementX) let result = try? AttributedString(mutableAttributedString, including: \.elementX)
Self.caches[cacheKey]?.setValue(result, forKey: htmlString) Self.cacheValue(result, forKey: htmlString, cacheKey: cacheKey)
return result return result
} }
// MARK: - Private // MARK: - Private
private static func cacheValue(_ value: AttributedString?, forKey key: String, cacheKey: String) {
cacheDispatchQueue.sync {
if caches[cacheKey] == nil {
caches[cacheKey] = LRUCache<String, AttributedString>(countLimit: 1000)
}
caches[cacheKey]?.setValue(value, forKey: key)
}
}
private static func cachedValue(forKey key: String, cacheKey: String) -> AttributedString? {
var result: AttributedString?
cacheDispatchQueue.sync {
result = caches[cacheKey]?.value(forKey: key)
}
return result
}
private func replaceMarkedBlockquotes(_ attributedString: NSMutableAttributedString) { private func replaceMarkedBlockquotes(_ attributedString: NSMutableAttributedString) {
// According to blockquotes in the string, DTCoreText can apply 2 policies: // According to blockquotes in the string, DTCoreText can apply 2 policies:
// - define a `DTTextBlocksAttribute` attribute on a <blockquote> block // - define a `DTTextBlocksAttribute` attribute on a <blockquote> block