Skip to content

Commit 963de65

Browse files
authored
Do not cache NSLocale.current if we fail to fetch preferences (#1134)
Currently, we do not cache the current Swift locale if we cannot fetch proper preferences. This allows us to update the cached value if subsequent access succeeds. We should be doing that for `currentNSLocale` too, but we are not. Fix the logic so that the behaviors match. Resolves rdar://142699797
1 parent 8d56fb7 commit 963de65

File tree

1 file changed

+58
-48
lines changed

1 file changed

+58
-48
lines changed

Sources/FoundationEssentials/Locale/Locale_Cache.swift

Lines changed: 58 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -34,30 +34,30 @@ dynamic package func _localeICUClass() -> _LocaleProtocol.Type {
3434
/// Singleton which listens for notifications about preference changes for Locale and holds cached singletons.
3535
struct LocaleCache : Sendable, ~Copyable {
3636
// MARK: - State
37-
37+
3838
struct State {
39-
39+
4040
init() {
4141
#if FOUNDATION_FRAMEWORK
4242
// For Foundation.framework, we listen for system notifications about the system Locale changing from the Darwin notification center.
4343
_CFNotificationCenterInitializeDependentNotificationIfNecessary(CFNotificationName.cfLocaleCurrentLocaleDidChange!.rawValue)
4444
#endif
4545
}
46-
46+
4747
private var cachedFixedLocales: [String : any _LocaleProtocol] = [:]
4848
private var cachedFixedComponentsLocales: [Locale.Components : any _LocaleProtocol] = [:]
4949

5050
#if FOUNDATION_FRAMEWORK
5151
private var cachedFixedIdentifierToNSLocales: [String : _NSSwiftLocale] = [:]
52-
52+
5353
struct IdentifierAndPrefs : Hashable {
5454
let identifier: String
5555
let prefs: LocalePreferences?
5656
}
57-
57+
5858
private var cachedFixedLocaleToNSLocales: [IdentifierAndPrefs : _NSSwiftLocale] = [:]
5959
#endif
60-
60+
6161
mutating func fixed(_ id: String) -> any _LocaleProtocol {
6262
// Note: Even if the currentLocale's identifier is the same, currentLocale may have preference overrides which are not reflected in the identifier itself.
6363
if let locale = cachedFixedLocales[id] {
@@ -81,7 +81,7 @@ struct LocaleCache : Sendable, ~Copyable {
8181
return locale
8282
}
8383
}
84-
84+
8585
#if canImport(_FoundationICU)
8686
mutating func fixedNSLocale(_ locale: _LocaleICU) -> _NSSwiftLocale {
8787
let id = IdentifierAndPrefs(identifier: locale.identifier, prefs: locale.prefs)
@@ -102,33 +102,33 @@ struct LocaleCache : Sendable, ~Copyable {
102102
func fixedComponents(_ comps: Locale.Components) -> (any _LocaleProtocol)? {
103103
cachedFixedComponentsLocales[comps]
104104
}
105-
105+
106106
mutating func fixedComponentsWithCache(_ comps: Locale.Components) -> any _LocaleProtocol {
107107
if let l = fixedComponents(comps) {
108108
return l
109109
} else {
110110
let new = _localeICUClass().init(components: comps)
111-
111+
112112
cachedFixedComponentsLocales[comps] = new
113113
return new
114114
}
115115
}
116116
}
117117

118118
let lock: LockedState<State>
119-
119+
120120
static let cache = LocaleCache()
121121
private let _currentCache = LockedState<(any _LocaleProtocol)?>(initialState: nil)
122-
122+
123123
#if FOUNDATION_FRAMEWORK
124124
private var _currentNSCache = LockedState<_NSSwiftLocale?>(initialState: nil)
125125
#endif
126-
126+
127127
fileprivate init() {
128128
lock = LockedState(initialState: State())
129129
}
130130

131-
131+
132132
/// For testing of `autoupdatingCurrent` only. If you want to test `current`, create a custom `Locale` with the appropriate settings using `localeAsIfCurrent(name:overrides:disableBundleMatching:)` and use that instead.
133133
/// This mutates global state of the current locale, so it is not safe to use in concurrent testing.
134134
func resetCurrent(to preferences: LocalePreferences) {
@@ -150,32 +150,36 @@ struct LocaleCache : Sendable, ~Copyable {
150150
}
151151

152152
var current: any _LocaleProtocol {
153+
return _currentAndCache.locale
154+
}
155+
156+
fileprivate var _currentAndCache: (locale: any _LocaleProtocol, doCache: Bool) {
153157
if let result = _currentCache.withLock({ $0 }) {
154-
return result
158+
return (result, true)
155159
}
156-
160+
157161
// We need to fetch prefs and try again
158162
let (preferences, doCache) = preferences()
159163
let locale = _localeICUClass().init(name: nil, prefs: preferences, disableBundleMatching: false)
160-
164+
161165
// It's possible this was an 'incomplete locale', in which case we will want to calculate it again later.
162166
if doCache {
163167
return _currentCache.withLock {
164168
if let current = $0 {
165169
// Someone beat us to setting it - use existing one
166-
return current
170+
return (current, true)
167171
} else {
168172
$0 = locale
169-
return locale
173+
return (locale, true)
170174
}
171175
}
176+
} else {
177+
return (locale, false)
172178
}
173-
174-
return locale
175179
}
176-
180+
177181
// MARK: Singletons
178-
182+
179183
// This value is immutable, so we can share one instance for the whole process.
180184
static let unlocalized = _LocaleUnlocalized(identifier: "en_001")
181185

@@ -185,19 +189,19 @@ struct LocaleCache : Sendable, ~Copyable {
185189
static let system : any _LocaleProtocol = {
186190
_localeICUClass().init(identifier: "", prefs: nil)
187191
}()
188-
192+
189193
#if FOUNDATION_FRAMEWORK
190194
static let autoupdatingCurrentNSLocale : _NSSwiftLocale = {
191195
_NSSwiftLocale(Locale(inner: autoupdatingCurrent))
192196
}()
193-
197+
194198
static let systemNSLocale : _NSSwiftLocale = {
195199
_NSSwiftLocale(Locale(inner: system))
196200
}()
197201
#endif
198-
202+
199203
// MARK: -
200-
204+
201205
func fixed(_ id: String) -> any _LocaleProtocol {
202206
lock.withLock {
203207
$0.fixed(id)
@@ -219,19 +223,25 @@ struct LocaleCache : Sendable, ~Copyable {
219223
if let result = _currentNSCache.withLock({ $0 }) {
220224
return result
221225
}
222-
226+
223227
// Create the current _NSSwiftLocale, based on the current Swift Locale.
228+
// n.b. do not call just `current` here; instead, use `_currentAndCache`
229+
// so that the caching status is honored
230+
let (current, doCache) = _currentAndCache
224231
let nsLocale = _NSSwiftLocale(Locale(inner: current))
225-
226-
// TODO: The current locale has an idea of not caching, which we have never honored here in the NSLocale cache
227-
return _currentNSCache.withLock {
228-
if let current = $0 {
229-
// Someone beat us to setting it, use that one
230-
return current
231-
} else {
232-
$0 = nsLocale
233-
return nsLocale
232+
233+
if doCache {
234+
return _currentNSCache.withLock {
235+
if let current = $0 {
236+
// Someone beat us to setting it, use that one
237+
return current
238+
} else {
239+
$0 = nsLocale
240+
return nsLocale
241+
}
234242
}
243+
} else {
244+
return nsLocale
235245
}
236246
}
237247

@@ -240,7 +250,7 @@ struct LocaleCache : Sendable, ~Copyable {
240250
func fixedComponents(_ comps: Locale.Components) -> any _LocaleProtocol {
241251
lock.withLock { $0.fixedComponentsWithCache(comps) }
242252
}
243-
253+
244254
#if FOUNDATION_FRAMEWORK && !NO_CFPREFERENCES
245255
func preferences() -> (LocalePreferences, Bool) {
246256
// On Darwin, we check the current user preferences for Locale values
@@ -249,28 +259,28 @@ struct LocaleCache : Sendable, ~Copyable {
249259

250260
var prefs = LocalePreferences()
251261
prefs.apply(cfPrefs)
252-
262+
253263
if wouldDeadlock.boolValue {
254264
// Don't cache a locale built with incomplete prefs
255265
return (prefs, false)
256266
} else {
257267
return (prefs, true)
258268
}
259269
}
260-
270+
261271
func preferredLanguages(forCurrentUser: Bool) -> [String] {
262272
var languages: [String] = []
263273
if forCurrentUser {
264274
languages = CFPreferencesCopyValue("AppleLanguages" as CFString, kCFPreferencesAnyApplication, kCFPreferencesCurrentUser, kCFPreferencesAnyHost) as? [String] ?? []
265275
} else {
266276
languages = CFPreferencesCopyAppValue("AppleLanguages" as CFString, kCFPreferencesCurrentApplication) as? [String] ?? []
267277
}
268-
278+
269279
return languages.compactMap {
270280
Locale.canonicalLanguageIdentifier(from: $0)
271281
}
272282
}
273-
283+
274284
func preferredLocale() -> String? {
275285
guard let preferredLocaleID = CFPreferencesCopyAppValue("AppleLocale" as CFString, kCFPreferencesCurrentApplication) as? String else {
276286
return nil
@@ -288,29 +298,29 @@ struct LocaleCache : Sendable, ~Copyable {
288298
func preferredLanguages(forCurrentUser: Bool) -> [String] {
289299
[Locale.canonicalLanguageIdentifier(from: "en-001")]
290300
}
291-
301+
292302
func preferredLocale() -> String? {
293303
"en_001"
294304
}
295305
#endif
296-
306+
297307
#if FOUNDATION_FRAMEWORK && !NO_CFPREFERENCES
298308
/// This returns an instance of `Locale` that's set up exactly like it would be if the user changed the current locale to that identifier, set the preferences keys in the overrides dictionary, then called `current`.
299309
func localeAsIfCurrent(name: String?, cfOverrides: CFDictionary? = nil, disableBundleMatching: Bool = false) -> Locale {
300-
310+
301311
var (prefs, _) = preferences()
302312
if let cfOverrides { prefs.apply(cfOverrides) }
303-
313+
304314
let inner = _LocaleICU(name: name, prefs: prefs, disableBundleMatching: disableBundleMatching)
305315
return Locale(inner: inner)
306316
}
307317
#endif
308-
318+
309319
/// This returns an instance of `Locale` that's set up exactly like it would be if the user changed the current locale to that identifier, set the preferences keys in the overrides dictionary, then called `current`.
310320
func localeAsIfCurrent(name: String?, overrides: LocalePreferences? = nil, disableBundleMatching: Bool = false) -> Locale {
311321
var (prefs, _) = preferences()
312322
if let overrides { prefs.apply(overrides) }
313-
323+
314324
let inner = _localeICUClass().init(name: name, prefs: prefs, disableBundleMatching: disableBundleMatching)
315325
return Locale(inner: inner)
316326
}
@@ -334,7 +344,7 @@ struct LocaleCache : Sendable, ~Copyable {
334344

335345
let preferredLanguages = preferredLanguages(forCurrentUser: false)
336346
guard let preferredLocaleID = preferredLocale() else { return nil }
337-
347+
338348
let canonicalizedLocalizations = availableLocalizations.compactMap { Locale.canonicalLanguageIdentifier(from: $0) }
339349
let identifier = Locale.localeIdentifierForCanonicalizedLocalizations(canonicalizedLocalizations, preferredLanguages: preferredLanguages, preferredLocaleID: preferredLocaleID)
340350
guard let identifier else {

0 commit comments

Comments
 (0)