Skip to content

Commit 9990c28

Browse files
parkeraCharles Hu
authored andcommitted
rdar://106190030 (Fix crash)
1 parent 1fe6e1f commit 9990c28

File tree

4 files changed

+285
-247
lines changed

4 files changed

+285
-247
lines changed

Sources/FoundationInternationalization/Locale/Locale.swift

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -118,26 +118,19 @@ public struct Locale : Hashable, Equatable, Sendable {
118118
#if FOUNDATION_FRAMEWORK
119119
/// 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`.
120120
internal static func localeAsIfCurrent(name: String?, cfOverrides: CFDictionary? = nil, disableBundleMatching: Bool = false) -> Locale {
121-
let (inner, _) = _Locale._currentLocaleWithCFOverrides(name: name, overrides: cfOverrides, disableBundleMatching: disableBundleMatching)
122-
return Locale(.fixed(inner))
121+
return LocaleCache.cache.localeAsIfCurrent(name: name, cfOverrides: cfOverrides, disableBundleMatching: disableBundleMatching)
123122
}
124123
#endif
125124
/// 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`.
126125
internal static func localeAsIfCurrent(name: String?, overrides: LocalePreferences? = nil, disableBundleMatching: Bool = false) -> Locale {
127126
// On Darwin, this overrides are applied on top of CFPreferences.
128-
let (inner, _) = _Locale._currentLocaleWithOverrides(name: name, overrides: overrides, disableBundleMatching: disableBundleMatching)
129-
return Locale(.fixed(inner))
127+
return LocaleCache.cache.localeAsIfCurrent(name: name, overrides: overrides, disableBundleMatching: disableBundleMatching)
130128
}
131129

132-
133130
internal static func localeAsIfCurrentWithBundleLocalizations(_ availableLocalizations: [String], allowsMixedLocalizations: Bool) -> Locale? {
134-
guard let inner = _Locale._currentLocaleWithBundleLocalizations(availableLocalizations, allowsMixedLocalizations: allowsMixedLocalizations) else {
135-
return nil
136-
}
137-
return Locale(.fixed(inner))
131+
return LocaleCache.cache.localeAsIfCurrentWithBundleLocalizations(availableLocalizations, allowsMixedLocalizations: allowsMixedLocalizations)
138132
}
139133

140-
141134
// MARK: -
142135
//
143136

@@ -164,7 +157,8 @@ public struct Locale : Hashable, Equatable, Sendable {
164157
self = .init(components: comps)
165158
}
166159

167-
private init(_ kind: Kind) {
160+
/// To be used only by `LocaleCache`.
161+
internal init(_ kind: Kind) {
168162
self.kind = kind
169163
}
170164

@@ -926,7 +920,7 @@ public struct Locale : Hashable, Equatable, Sendable {
926920
/// - seealso: `Bundle.preferredLocalizations(from:)`
927921
/// - seealso: `Bundle.preferredLocalizations(from:forPreferences:)`
928922
public static var preferredLanguages: [String] {
929-
_Locale.preferredLanguages(forCurrentUser: false)
923+
LocaleCache.cache.preferredLanguages(forCurrentUser: false)
930924
}
931925

932926

Sources/FoundationInternationalization/Locale/Locale_Cache.swift

Lines changed: 165 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -60,18 +60,26 @@ struct LocaleCache : Sendable {
6060
}
6161
}
6262

63-
mutating func current() -> _Locale {
63+
mutating func current(preferences: LocalePreferences?, cache: Bool) -> _Locale? {
6464
resetCurrentIfNeeded()
6565

6666
if let cachedCurrentLocale {
6767
return cachedCurrentLocale
68-
} else {
69-
let (locale, doCache) = _Locale._currentLocaleWithOverrides(name: nil, overrides: nil, disableBundleMatching: false)
70-
if doCache {
71-
self.cachedCurrentLocale = locale
72-
}
73-
return locale
7468
}
69+
70+
// At this point we know we need to create, or re-create, the Locale instance.
71+
// If we do not have a set of preferences to use, we have to return nil.
72+
guard let preferences else {
73+
return nil
74+
}
75+
76+
let locale = _Locale(name: nil, prefs: preferences, disableBundleMatching: false)
77+
if cache {
78+
// It's possible this was an 'incomplete locale', in which case we will want to calculate it again later.
79+
self.cachedCurrentLocale = locale
80+
}
81+
82+
return locale
7583
}
7684

7785
mutating func fixed(_ id: String) -> _Locale {
@@ -98,7 +106,7 @@ struct LocaleCache : Sendable {
98106
}
99107
}
100108

101-
mutating func currentNSLocale() -> _NSSwiftLocale {
109+
mutating func currentNSLocale(preferences: LocalePreferences?, cache: Bool) -> _NSSwiftLocale? {
102110
resetCurrentIfNeeded()
103111

104112
if let currentNSLocale = cachedCurrentNSLocale {
@@ -108,17 +116,25 @@ struct LocaleCache : Sendable {
108116
let nsLocale = _NSSwiftLocale(Locale(inner: current))
109117
cachedCurrentNSLocale = nsLocale
110118
return nsLocale
111-
} else {
112-
// We have neither a Swift Locale nor an NSLocale. Recalculate and set both.
113-
let (locale, doCache) = _Locale._currentLocaleWithOverrides(name: nil, overrides: nil, disableBundleMatching: false)
114-
let nsLocale = _NSSwiftLocale(Locale(inner: locale))
115-
if doCache {
116-
// It's possible this was an 'incomplete locale', in which case we will want to calculate it again later.
117-
self.cachedCurrentLocale = locale
118-
cachedCurrentNSLocale = nsLocale
119-
}
120-
return nsLocale
121119
}
120+
121+
// At this point we know we need to create, or re-create, the Locale instance.
122+
123+
// If we do not have a set of preferences to use, we have to return nil.
124+
guard let preferences else {
125+
return nil
126+
}
127+
128+
// We have neither a Swift Locale nor an NSLocale. Recalculate and set both.
129+
let locale = _Locale(name: nil, prefs: preferences, disableBundleMatching: false)
130+
let nsLocale = _NSSwiftLocale(Locale(inner: locale))
131+
if cache {
132+
// It's possible this was an 'incomplete locale', in which case we will want to calculate it again later.
133+
self.cachedCurrentLocale = locale
134+
cachedCurrentNSLocale = nsLocale
135+
}
136+
137+
return nsLocale
122138
}
123139

124140
mutating func autoupdatingNSLocale() -> _NSSwiftLocale {
@@ -179,18 +195,30 @@ struct LocaleCache : Sendable {
179195
}
180196

181197
var current: _Locale {
182-
lock.withLock { $0.current() }
198+
var result = lock.withLock {
199+
$0.current(preferences: nil, cache: false)
200+
}
201+
202+
if let result { return result }
203+
204+
// We need to fetch prefs and try again
205+
let (prefs, doCache) = preferences()
206+
207+
result = lock.withLock {
208+
$0.current(preferences: prefs, cache: doCache)
209+
}
210+
211+
guard let result else {
212+
fatalError("Nil result getting current Locale with preferences")
213+
}
214+
215+
return result
183216
}
184217

185218
var system: _Locale {
186219
lock.withLock { $0.system() }
187220
}
188221

189-
var preferred: _Locale {
190-
let (locale, _) = _Locale._currentLocaleWithOverrides(name: nil, overrides: nil, disableBundleMatching: false)
191-
return locale
192-
}
193-
194222
func fixed(_ id: String) -> _Locale {
195223
lock.withLock { $0.fixed(id) }
196224
}
@@ -205,7 +233,24 @@ struct LocaleCache : Sendable {
205233
}
206234

207235
func currentNSLocale() -> _NSSwiftLocale {
208-
lock.withLock { $0.currentNSLocale() }
236+
var result = lock.withLock {
237+
$0.currentNSLocale(preferences: nil, cache: false)
238+
}
239+
240+
if let result { return result }
241+
242+
// We need to fetch prefs and try again. Don't do this inside a lock (106190030). On Darwin it is possible to get a KVO callout from fetching the preferences, which could ask for the current Locale, which could cause a reentrant lock.
243+
let (prefs, doCache) = preferences()
244+
245+
result = lock.withLock {
246+
$0.currentNSLocale(preferences: prefs, cache: doCache)
247+
}
248+
249+
guard let result else {
250+
fatalError("Nil result getting current NSLocale with preferences")
251+
}
252+
253+
return result
209254
}
210255

211256
func systemNSLocale() -> _NSSwiftLocale {
@@ -216,4 +261,99 @@ struct LocaleCache : Sendable {
216261
func fixedComponents(_ comps: Locale.Components) -> _Locale {
217262
lock.withLock { $0.fixedComponents(comps) }
218263
}
264+
265+
#if FOUNDATION_FRAMEWORK
266+
func preferences() -> (LocalePreferences, Bool) {
267+
// On Darwin, we check the current user preferences for Locale values
268+
var wouldDeadlock: DarwinBoolean = false
269+
let cfPrefs = __CFXPreferencesCopyCurrentApplicationStateWithDeadlockAvoidance(&wouldDeadlock).takeRetainedValue()
270+
271+
var prefs = LocalePreferences()
272+
prefs.apply(cfPrefs)
273+
274+
if wouldDeadlock.boolValue {
275+
// Don't cache a locale built with incomplete prefs
276+
return (prefs, false)
277+
} else {
278+
return (prefs, true)
279+
}
280+
}
281+
282+
func preferredLanguages(forCurrentUser: Bool) -> [String] {
283+
var languages: [String] = []
284+
if forCurrentUser {
285+
languages = CFPreferencesCopyValue("AppleLanguages" as CFString, kCFPreferencesAnyApplication, kCFPreferencesCurrentUser, kCFPreferencesAnyHost) as? [String] ?? []
286+
} else {
287+
languages = CFPreferencesCopyAppValue("AppleLanguages" as CFString, kCFPreferencesCurrentApplication) as? [String] ?? []
288+
}
289+
290+
return languages.compactMap {
291+
Locale.canonicalLanguageIdentifier(from: $0)
292+
}
293+
}
294+
295+
func preferredLocale() -> String? {
296+
guard let preferredLocaleID = CFPreferencesCopyAppValue("AppleLocale" as CFString, kCFPreferencesCurrentApplication) as? String else {
297+
return nil
298+
}
299+
return preferredLocaleID
300+
}
301+
#else
302+
func preferences() -> (LocalePreferences, Bool) {
303+
var prefs = LocalePreferences()
304+
prefs.locale = "en_US"
305+
prefs.languages = ["en-US"]
306+
return (prefs, true)
307+
}
308+
309+
func preferredLanguages(forCurrentUser: Bool) -> [String] {
310+
[Locale.canonicalLanguageIdentifier(from: "en-US")]
311+
}
312+
313+
func preferredLocale() -> String? {
314+
"en_US"
315+
}
316+
#endif
317+
318+
#if FOUNDATION_FRAMEWORK
319+
/// 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`.
320+
func localeAsIfCurrent(name: String?, cfOverrides: CFDictionary? = nil, disableBundleMatching: Bool = false) -> Locale {
321+
322+
var (prefs, _) = preferences()
323+
if let cfOverrides { prefs.apply(cfOverrides) }
324+
325+
let inner = _Locale(name: name, prefs: prefs, disableBundleMatching: disableBundleMatching)
326+
return Locale(.fixed(inner))
327+
}
328+
#endif
329+
330+
/// 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`.
331+
func localeAsIfCurrent(name: String?, overrides: LocalePreferences? = nil, disableBundleMatching: Bool = false) -> Locale {
332+
var (prefs, _) = preferences()
333+
if let overrides { prefs.apply(overrides) }
334+
335+
let inner = _Locale(name: name, prefs: prefs, disableBundleMatching: disableBundleMatching)
336+
return Locale(.fixed(inner))
337+
}
338+
339+
340+
func localeAsIfCurrentWithBundleLocalizations(_ availableLocalizations: [String], allowsMixedLocalizations: Bool) -> Locale? {
341+
guard !allowsMixedLocalizations else {
342+
let (prefs, _) = preferences()
343+
let inner = _Locale(name: nil, prefs: prefs, disableBundleMatching: true)
344+
return Locale(.fixed(inner))
345+
}
346+
347+
let preferredLanguages = preferredLanguages(forCurrentUser: false)
348+
guard let preferredLocaleID = preferredLocale() else { return nil }
349+
350+
let canonicalizedLocalizations = availableLocalizations.compactMap { Locale.canonicalLanguageIdentifier(from: $0) }
351+
let identifier = _Locale.localeIdentifierForCanonicalizedLocalizations(canonicalizedLocalizations, preferredLanguages: preferredLanguages, preferredLocaleID: preferredLocaleID)
352+
guard let identifier else {
353+
return nil
354+
}
355+
356+
let inner = _Locale(identifier: identifier)
357+
return Locale(.fixed(inner))
358+
}
219359
}

0 commit comments

Comments
 (0)