Skip to content

Commit 7ce7be4

Browse files
authored
Handle ICU failures better (#625)
Instead of assuming ICU calls always succeed, surface the error back to callsites and fallback to some default value if we can't get ICU formatters. Resovles 125918488
1 parent b2f4b02 commit 7ce7be4

File tree

12 files changed

+158
-88
lines changed

12 files changed

+158
-88
lines changed

Sources/FoundationInternationalization/Calendar/Calendar_ICU.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1959,7 +1959,11 @@ internal final class _CalendarICU: _CalendarProtocol, @unchecked Sendable {
19591959
@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *)
19601960
extension Calendar {
19611961
private func symbols(for key: UDateFormatSymbolType) -> [String] {
1962-
ICUDateFormatter.cachedFormatter(for: self).symbols(for: key)
1962+
guard let fmt = ICUDateFormatter.cachedFormatter(for: self) else {
1963+
return []
1964+
}
1965+
1966+
return fmt.symbols(for: key)
19631967
}
19641968

19651969
/// A list of eras in this calendar, localized to the Calendar's `locale`.

Sources/FoundationInternationalization/Formatting/ByteCountFormatStyle.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -259,13 +259,19 @@ private func localizedParens(locale: Locale) -> (String, String) {
259259
let ulocdata = locale.identifier.withCString {
260260
ulocdata_open($0, &status)
261261
}
262-
try! status.checkSuccess()
263262
defer { ulocdata_close(ulocdata) }
264263

264+
guard status.checkSuccessAndLogError("ulocdata_open failed.") else {
265+
return (" (", ")")
266+
}
267+
265268
let exemplars = ulocdata_getExemplarSet(ulocdata, nil, 0, .punctuation, &status)
266-
try! status.checkSuccess()
267269
defer { uset_close(exemplars) }
268270

271+
guard status.checkSuccessAndLogError("ulocdata_getExemplarSet failed.") else {
272+
return (" (", ")")
273+
}
274+
269275
let fullwidthLeftParenUTF32 = 0x0000FF08 as Int32
270276
let containsFullWidth = uset_contains(exemplars!, fullwidthLeftParenUTF32).boolValue
271277

Sources/FoundationInternationalization/Formatting/Date/Date+IntervalFormatStyle.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,10 @@ extension Date {
5656
// MARK: - FormatStyle conformance
5757

5858
public func format(_ v: Range<Date>) -> String {
59-
ICUDateIntervalFormatter.formatter(for: self).string(from: v)
59+
guard let formatter = ICUDateIntervalFormatter.formatter(for: self), let result = formatter.string(from: v) else {
60+
return "\(v.lowerBound.description) - \(v.upperBound.description)"
61+
}
62+
return result
6063
}
6164

6265
public func locale(_ locale: Locale) -> Self {

Sources/FoundationInternationalization/Formatting/Date/Date+VerbatimFormatStyle.swift

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@ extension Date {
4444
}
4545

4646
public func format(_ value: Date) -> String {
47-
return ICUDateFormatter.cachedFormatter(for: self).format(value) ?? value.description
47+
guard let fm = ICUDateFormatter.cachedFormatter(for: self), let result = fm.format(value) else {
48+
return value.description
49+
}
50+
51+
return result
4852
}
4953

5054
public func locale(_ locale: Locale) -> Date.VerbatimFormatStyle {
@@ -101,16 +105,11 @@ extension Date.VerbatimFormatStyle {
101105
}
102106

103107
public func format(_ value: Date) -> AttributedString {
104-
let fm = ICUDateFormatter.cachedFormatter(for: base)
105-
106-
var result: AttributedString
107-
if let (str, attributes) = fm.attributedFormat(value) {
108-
result = str._attributedStringFromPositions(attributes)
109-
} else {
110-
result = AttributedString(value.description)
108+
guard let fm = ICUDateFormatter.cachedFormatter(for: base), let (str, attributes) = fm.attributedFormat(value) else {
109+
return AttributedString(value.description)
111110
}
112111

113-
return result
112+
return str._attributedStringFromPositions(attributes)
114113
}
115114

116115
public func locale(_ locale: Locale) -> Self {

Sources/FoundationInternationalization/Formatting/Date/DateFormatStyle.swift

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -306,22 +306,19 @@ extension Date {
306306

307307
/// Returns an attributed string with `AttributeScopes.FoundationAttributes.DateFieldAttribute`
308308
public func format(_ value: Date) -> AttributedString {
309-
let fm: ICUDateFormatter
309+
let fm: ICUDateFormatter?
310310
switch innerStyle {
311311
case .formatStyle(let formatStyle):
312312
fm = ICUDateFormatter.cachedFormatter(for: formatStyle)
313313
case .verbatimFormatStyle(let verbatimFormatStyle):
314314
fm = ICUDateFormatter.cachedFormatter(for: verbatimFormatStyle)
315315
}
316316

317-
var result: AttributedString
318-
if let (str, attributes) = fm.attributedFormat(value) {
319-
result = str._attributedStringFromPositions(attributes)
320-
} else {
321-
result = AttributedString("")
317+
guard let fm, let (str, attributes) = fm.attributedFormat(value) else {
318+
return AttributedString("")
322319
}
323-
324-
return result
320+
321+
return str._attributedStringFromPositions(attributes)
325322
}
326323

327324
public func locale(_ locale: Locale) -> Self {
@@ -376,16 +373,10 @@ extension Date.FormatStyle {
376373
}
377374

378375
public func format(_ value: Date) -> AttributedString {
379-
let fm = ICUDateFormatter.cachedFormatter(for: base)
380-
381-
var result: AttributedString
382-
if let (str, attributes) = fm.attributedFormat(value) {
383-
result = str._attributedStringFromPositions(attributes)
384-
} else {
385-
result = AttributedString("")
376+
guard let fm = ICUDateFormatter.cachedFormatter(for: base), let (str, attributes) = fm.attributedFormat(value) else {
377+
return AttributedString("")
386378
}
387-
388-
return result
379+
return str._attributedStringFromPositions(attributes)
389380
}
390381

391382
public func locale(_ locale: Locale) -> Self {
@@ -691,8 +682,10 @@ extension Date.FormatStyle.Attributed {
691682
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
692683
extension Date.FormatStyle : FormatStyle {
693684
public func format(_ value: Date) -> String {
694-
let fm = ICUDateFormatter.cachedFormatter(for: self)
695-
return fm.format(value) ?? ""
685+
guard let fm = ICUDateFormatter.cachedFormatter(for: self), let result = fm.format(value) else {
686+
return ""
687+
}
688+
return result
696689
}
697690

698691
public func locale(_ locale: Locale) -> Self {
@@ -707,7 +700,10 @@ extension Date.FormatStyle : FormatStyle {
707700
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
708701
extension Date.FormatStyle : ParseStrategy {
709702
public func parse(_ value: String) throws -> Date {
710-
let fm = ICUDateFormatter.cachedFormatter(for: self)
703+
guard let fm = ICUDateFormatter.cachedFormatter(for: self) else {
704+
throw CocoaError(CocoaError.formatting, userInfo: [ NSDebugDescriptionErrorKey: "Error creating icu date formatter" ])
705+
}
706+
711707
guard let date = fm.parse(value) else {
712708
throw parseError(value, exampleFormattedString: fm.format(Date.now))
713709
}
@@ -1093,7 +1089,10 @@ extension Date.FormatStyle : CustomConsumingRegexComponent {
10931089
guard index < bounds.upperBound else {
10941090
return nil
10951091
}
1096-
return ICUDateFormatter.cachedFormatter(for: self).parse(input, in: index..<bounds.upperBound)
1092+
guard let fmt = ICUDateFormatter.cachedFormatter(for: self) else {
1093+
return nil
1094+
}
1095+
return fmt.parse(input, in: index..<bounds.upperBound)
10971096
}
10981097
}
10991098

Sources/FoundationInternationalization/Formatting/Date/DateParseStrategy.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ extension Date {
5858
self.twoDigitStartDate = twoDigitStartDate
5959
}
6060

61-
private var formatter: ICUDateFormatter {
61+
private var formatter: ICUDateFormatter? {
6262
let dateFormatInfo = ICUDateFormatter.DateFormatInfo(localeIdentifier: locale?.identifier, timeZoneIdentifier: timeZone.identifier, calendarIdentifier: calendar.identifier, firstWeekday: calendar.firstWeekday, minimumDaysInFirstWeek: calendar.minimumDaysInFirstWeek, capitalizationContext: .unknown, pattern: format, parseLenient: isLenient, parseTwoDigitStartDate: twoDigitStartDate)
6363
return ICUDateFormatter.cachedFormatter(for: dateFormatInfo)
6464
}
@@ -77,6 +77,10 @@ extension Date.ParseStrategy : ParseStrategy {
7777
/// - Throws: Throws `NSFormattingError` if the string cannot be parsed.
7878
/// - Returns: A `Date` represented by `value`.
7979
public func parse(_ value: String) throws -> Date {
80+
guard let formatter = self.formatter else {
81+
throw CocoaError(CocoaError.formatting, userInfo: [ NSDebugDescriptionErrorKey: "Error creating icu date formatter" ])
82+
}
83+
8084
guard let date = formatter.parse(value) else {
8185
throw parseError(value, exampleFormattedString: formatter.format(Date.now))
8286
}
@@ -101,7 +105,10 @@ extension Date.ParseStrategy : CustomConsumingRegexComponent {
101105
guard index < bounds.upperBound else {
102106
return nil
103107
}
104-
return formatter.parse(input, in: index..<bounds.upperBound)
108+
guard let fmt = self.formatter else {
109+
return nil
110+
}
111+
return fmt.parse(input, in: index..<bounds.upperBound)
105112
}
106113
}
107114

Sources/FoundationInternationalization/Formatting/Date/ICUDateFormatter.swift

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ final class ICUDateFormatter {
2929
var udateFormat: UnsafeMutablePointer<UDateFormat?>
3030
var lenientParsing: Bool
3131

32-
private init(localeIdentifier: String, timeZoneIdentifier: String, calendarIdentifier: Calendar.Identifier, firstWeekday: Int, minimumDaysInFirstWeek: Int, capitalizationContext: FormatStyleCapitalizationContext, pattern: String, twoDigitStartDate: Date, lenientParsing: Bool) {
32+
private init?(localeIdentifier: String, timeZoneIdentifier: String, calendarIdentifier: Calendar.Identifier, firstWeekday: Int, minimumDaysInFirstWeek: Int, capitalizationContext: FormatStyleCapitalizationContext, pattern: String, twoDigitStartDate: Date, lenientParsing: Bool) {
3333
self.lenientParsing = lenientParsing
3434

3535
// We failed to construct a locale with the given calendar; fall back to locale's identifier
@@ -39,34 +39,44 @@ final class ICUDateFormatter {
3939
let pt = Array(pattern.utf16)
4040

4141
var status = U_ZERO_ERROR
42-
udateFormat = udat_open(UDAT_PATTERN, UDAT_PATTERN, localeIdentifierWithCalendar, tz, Int32(tz.count), pt, Int32(pt.count), &status)!
43-
try! status.checkSuccess()
42+
let udat = udat_open(UDAT_PATTERN, UDAT_PATTERN, localeIdentifierWithCalendar, tz, Int32(tz.count), pt, Int32(pt.count), &status)
43+
44+
guard status.checkSuccessAndLogError("udat_open failed."), let udat else {
45+
if (udat != nil) {
46+
udat_close(udat)
47+
}
48+
return nil
49+
}
50+
51+
udateFormat = udat
4452

4553
udat_setContext(udateFormat, capitalizationContext.icuContext, &status)
46-
try! status.checkSuccess()
54+
_ = status.checkSuccessAndLogError("udat_setContext failed.")
4755

4856
if lenientParsing {
4957
udat_setLenient(udateFormat, UBool.true)
5058
} else {
5159
udat_setLenient(udateFormat, UBool.false)
5260

5361
udat_setBooleanAttribute(udateFormat, UDAT_PARSE_ALLOW_WHITESPACE, UBool.false, &status)
54-
try! status.checkSuccess()
62+
_ = status.checkSuccessAndLogError("Cannot set UDAT_PARSE_ALLOW_WHITESPACE.")
5563

5664
udat_setBooleanAttribute(udateFormat, UDAT_PARSE_ALLOW_NUMERIC, UBool.false, &status)
57-
try! status.checkSuccess()
65+
_ = status.checkSuccessAndLogError("Cannot set UDAT_PARSE_ALLOW_NUMERIC.")
5866

5967
udat_setBooleanAttribute(udateFormat, UDAT_PARSE_PARTIAL_LITERAL_MATCH, UBool.false, &status)
60-
try! status.checkSuccess()
68+
_ = status.checkSuccessAndLogError("Cannot set UDAT_PARSE_PARTIAL_LITERAL_MATCH.")
6169

6270
udat_setBooleanAttribute(udateFormat, UDAT_PARSE_MULTIPLE_PATTERNS_FOR_MATCH, UBool.false, &status)
63-
try! status.checkSuccess()
71+
_ = status.checkSuccessAndLogError("Cannot set UDAT_PARSE_MULTIPLE_PATTERNS_FOR_MATCH.")
6472
}
6573

6674
let udatCalendar = udat_getCalendar(udateFormat)
6775
let ucal = ucal_clone(udatCalendar, &status)
6876
defer { ucal_close(ucal) }
69-
try! status.checkSuccess()
77+
guard status.checkSuccessAndLogError("ucal_clone failed."), let ucal else {
78+
return
79+
}
7080

7181
ucal_clear(ucal)
7282
ucal_setAttribute(ucal, .firstDayOfWeek, Int32(firstWeekday))
@@ -227,10 +237,6 @@ final class ICUDateFormatter {
227237
var parseLenient: Bool
228238
var parseTwoDigitStartDate: Date
229239

230-
func createICUDateFormatter() -> ICUDateFormatter {
231-
ICUDateFormatter(localeIdentifier: localeIdentifier, timeZoneIdentifier: timeZoneIdentifier, calendarIdentifier: calendarIdentifier, firstWeekday: firstWeekday, minimumDaysInFirstWeek: minimumDaysInFirstWeek, capitalizationContext: capitalizationContext, pattern: pattern, twoDigitStartDate: parseTwoDigitStartDate, lenientParsing: parseLenient)
232-
}
233-
234240
init(localeIdentifier: String?, timeZoneIdentifier: String, calendarIdentifier: Calendar.Identifier, firstWeekday: Int, minimumDaysInFirstWeek: Int, capitalizationContext: FormatStyleCapitalizationContext, pattern: String, parseLenient: Bool = true, parseTwoDigitStartDate: Date = Date(timeIntervalSince1970: 0)) {
235241
if let localeIdentifier {
236242
self.localeIdentifier = localeIdentifier
@@ -250,11 +256,13 @@ final class ICUDateFormatter {
250256
}
251257
}
252258

253-
static let formatterCache = FormatterCache<DateFormatInfo, ICUDateFormatter>()
259+
static let formatterCache = FormatterCache<DateFormatInfo, ICUDateFormatter?>()
254260
static var patternCache = LockedState<[PatternCacheKey : String]>(initialState: [:])
255261

256-
static func cachedFormatter(for dateFormatInfo: DateFormatInfo) -> ICUDateFormatter {
257-
return Self.formatterCache.formatter(for: dateFormatInfo, creator: dateFormatInfo.createICUDateFormatter)
262+
static func cachedFormatter(for dateFormatInfo: DateFormatInfo) -> ICUDateFormatter? {
263+
return Self.formatterCache.formatter(for: dateFormatInfo) {
264+
ICUDateFormatter(localeIdentifier: dateFormatInfo.localeIdentifier, timeZoneIdentifier: dateFormatInfo.timeZoneIdentifier, calendarIdentifier: dateFormatInfo.calendarIdentifier, firstWeekday: dateFormatInfo.firstWeekday, minimumDaysInFirstWeek: dateFormatInfo.minimumDaysInFirstWeek, capitalizationContext: dateFormatInfo.capitalizationContext, pattern: dateFormatInfo.pattern, twoDigitStartDate: dateFormatInfo.parseTwoDigitStartDate, lenientParsing: dateFormatInfo.parseLenient)
265+
}
258266
}
259267

260268
struct PatternCacheKey : Hashable {
@@ -264,17 +272,17 @@ final class ICUDateFormatter {
264272
var datePatternOverride: String?
265273
}
266274

267-
static func cachedFormatter(for format: Date.FormatStyle) -> ICUDateFormatter {
268-
return cachedFormatter(for: .init(format))
275+
static func cachedFormatter(for format: Date.FormatStyle) -> ICUDateFormatter? {
276+
cachedFormatter(for: .init(format))
269277
}
270278

271-
static func cachedFormatter(for format: Date.VerbatimFormatStyle) -> ICUDateFormatter {
272-
return cachedFormatter(for: .init(format))
279+
static func cachedFormatter(for format: Date.VerbatimFormatStyle) -> ICUDateFormatter? {
280+
cachedFormatter(for: .init(format))
273281
}
274282

275283
// Returns a formatter to retrieve localized calendar symbols
276-
static func cachedFormatter(for calendar: Calendar) -> ICUDateFormatter {
277-
return cachedFormatter(for: .init(calendar))
284+
static func cachedFormatter(for calendar: Calendar) -> ICUDateFormatter? {
285+
cachedFormatter(for: .init(calendar))
278286
}
279287
}
280288

Sources/FoundationInternationalization/Formatting/Date/ICUDateIntervalFormatter.swift

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ final class ICUDateIntervalFormatter {
2424
let dateTemplate: String
2525
}
2626

27-
internal static let cache = FormatterCache<Signature, ICUDateIntervalFormatter>()
27+
internal static let cache = FormatterCache<Signature, ICUDateIntervalFormatter?>()
2828

2929
let uformatter: OpaquePointer // UDateIntervalFormat
3030

31-
private init(signature: Signature) {
31+
private init?(signature: Signature) {
3232
var comps = signature.localeComponents
3333
comps.calendar = signature.calendarIdentifier
3434
let id = comps.icuIdentifier
@@ -37,36 +37,39 @@ final class ICUDateIntervalFormatter {
3737
let dateTemplate16 = Array(signature.dateTemplate.utf16)
3838

3939
var status = U_ZERO_ERROR
40-
uformatter = tz16.withUnsafeBufferPointer { tz in
40+
let formatter = tz16.withUnsafeBufferPointer { tz in
4141
dateTemplate16.withUnsafeBufferPointer { template in
4242
udtitvfmt_open(id, template.baseAddress, Int32(template.count), tz.baseAddress, Int32(tz.count), &status)
4343
}
4444
}
4545

46-
try! status.checkSuccess()
46+
guard status.checkSuccessAndLogError("udtitvfmt_open failed."), let formatter else {
47+
if (formatter != nil) {
48+
udtitvfmt_close(formatter)
49+
}
50+
return nil
51+
}
4752

48-
udtitvfmt_setAttribute(uformatter, UDTITVFMT_MINIMIZE_TYPE, UDTITVFMT_MINIMIZE_NONE, &status)
53+
uformatter = formatter
4954

50-
try! status.checkSuccess()
55+
udtitvfmt_setAttribute(uformatter, UDTITVFMT_MINIMIZE_TYPE, UDTITVFMT_MINIMIZE_NONE, &status)
56+
_ = status.checkSuccessAndLogError("udtitvfmt_setAttribute failed.")
5157
}
5258

5359
deinit {
5460
udtitvfmt_close(uformatter)
5561
}
5662

57-
func string(from: Range<Date>) -> String {
63+
func string(from: Range<Date>) -> String? {
5864
let fromUDate = from.lowerBound.udate
5965
let toUDate = from.upperBound.udate
6066

61-
let result = _withResizingUCharBuffer { buffer, size, status in
67+
return _withResizingUCharBuffer { buffer, size, status in
6268
udtitvfmt_format(uformatter, fromUDate, toUDate, buffer, size, nil /* position */, &status)
6369
}
64-
65-
if let result { return result }
66-
return ""
6770
}
6871

69-
internal static func formatter(for style: Date.IntervalFormatStyle) -> ICUDateIntervalFormatter {
72+
internal static func formatter(for style: Date.IntervalFormatStyle) -> ICUDateIntervalFormatter? {
7073
var template = style.symbols.formatterTemplate(overridingDayPeriodWithLocale: style.locale)
7174

7275
if template.isEmpty {
@@ -80,9 +83,8 @@ final class ICUDateIntervalFormatter {
8083
let comps = Locale.Components(locale: style.locale)
8184
let signature = Signature(localeComponents: comps, calendarIdentifier: style.calendar.identifier, timeZoneIdentifier: style.timeZone.identifier, dateTemplate: template)
8285

83-
let formatter = Self.cache.formatter(for: signature) {
86+
return Self.cache.formatter(for: signature) {
8487
ICUDateIntervalFormatter(signature: signature)
8588
}
86-
return formatter
8789
}
8890
}

0 commit comments

Comments
 (0)