Skip to content

Commit ff3021f

Browse files
authored
fix(plugin-mongodb): prevent crash on NaN or infinite document values (#1418) (#1423)
* fix(plugin-mongodb): prevent crash on NaN or infinite document values (#1418) * refactor(plugin-mongodb): dedupe number handling and drop unreachable type cases in BSON flattener
1 parent ad1e2b8 commit ff3021f

4 files changed

Lines changed: 181 additions & 95 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
### Fixed
1515

1616
- Custom and OpenAI-compatible AI providers now work when the base URL already ends in `/v1`, instead of building a doubled `/v1/v1/` path that failed. (#1400)
17+
- MongoDB: opening a collection no longer crashes when a document contains a NaN or infinite number. (#1418)
1718

1819
## [0.45.0] - 2026-05-26
1920

Plugins/MongoDBDriverPlugin/BsonDocumentFlattener.swift

Lines changed: 57 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -73,23 +73,9 @@ struct BsonDocumentFlattener {
7373
case let str as String:
7474
return str
7575
case let num as NSNumber:
76-
// Check if it's a boolean (NSNumber wraps booleans too)
77-
if CFBooleanGetTypeID() == CFGetTypeID(num) {
78-
return num.boolValue ? "true" : "false"
79-
}
80-
return num.stringValue
81-
case let int as Int:
82-
return String(int)
83-
case let int32 as Int32:
84-
return String(int32)
85-
case let int64 as Int64:
86-
return String(int64)
87-
case let double as Double:
88-
return String(double)
89-
case let bool as Bool:
90-
return bool ? "true" : "false"
76+
return displayString(for: num)
9177
case let date as Date:
92-
return ISO8601DateFormatter().string(from: date)
78+
return iso8601Formatter.string(from: date)
9379
case let data as Data:
9480
return formatBinaryData(data)
9581
case let dict as [String: Any]:
@@ -121,24 +107,20 @@ struct BsonDocumentFlattener {
121107
/// Serialize a dictionary or array to compact JSON string
122108
static func serializeToJson(_ value: Any) -> String {
123109
let sanitized = sanitizeForJson(value)
124-
do {
125-
let data = try JSONSerialization.data(withJSONObject: sanitized, options: [.sortedKeys])
126-
if let json = String(data: data, encoding: .utf8) {
127-
// Cap at 10k chars to prevent mega-document display issues
128-
let nsJson = json as NSString
129-
if nsJson.length > 10_000 {
130-
return String(json.prefix(10_000)) + "..."
131-
}
132-
return json
133-
}
134-
} catch {
135-
// Fall through to description
110+
guard JSONSerialization.isValidJSONObject(sanitized),
111+
let data = try? JSONSerialization.data(withJSONObject: sanitized, options: [.sortedKeys]),
112+
let json = String(data: data, encoding: .utf8) else {
113+
return String(describing: value)
136114
}
137-
return String(describing: value)
115+
let nsJson = json as NSString
116+
if nsJson.length > 10_000 {
117+
return String(json.prefix(10_000)) + "..."
118+
}
119+
return json
138120
}
139121

140-
/// Recursively convert non-JSON-safe types (Data, Date, etc.) to JSON-safe representations
141-
private static func sanitizeForJson(_ value: Any) -> Any {
122+
/// Recursively convert every value into a JSON-safe representation
123+
static func sanitizeForJson(_ value: Any) -> Any {
142124
switch value {
143125
case let dict as [String: Any]:
144126
return dict.mapValues { sanitizeForJson($0) }
@@ -147,12 +129,50 @@ struct BsonDocumentFlattener {
147129
case let data as Data:
148130
return formatBinaryData(data)
149131
case let date as Date:
150-
return ISO8601DateFormatter().string(from: date)
151-
default:
132+
return iso8601Formatter.string(from: date)
133+
case is NSNull:
152134
return value
135+
case let str as String:
136+
return str
137+
case let num as NSNumber:
138+
return sanitizeNumber(num)
139+
default:
140+
return String(describing: value)
153141
}
154142
}
155143

144+
private static let iso8601Formatter = ISO8601DateFormatter()
145+
146+
private static func displayString(for num: NSNumber) -> String {
147+
if isBoolean(num) {
148+
return num.boolValue ? "true" : "false"
149+
}
150+
if isFloatingPoint(num), !num.doubleValue.isFinite {
151+
return nonFiniteToken(num.doubleValue)
152+
}
153+
return num.stringValue
154+
}
155+
156+
private static func sanitizeNumber(_ num: NSNumber) -> Any {
157+
guard !isBoolean(num) else { return num }
158+
guard isFloatingPoint(num), !num.doubleValue.isFinite else { return num }
159+
return nonFiniteToken(num.doubleValue)
160+
}
161+
162+
private static func isBoolean(_ num: NSNumber) -> Bool {
163+
CFBooleanGetTypeID() == CFGetTypeID(num)
164+
}
165+
166+
private static func isFloatingPoint(_ num: NSNumber) -> Bool {
167+
let objCType = String(cString: num.objCType)
168+
return objCType == "d" || objCType == "f"
169+
}
170+
171+
private static func nonFiniteToken(_ value: Double) -> String {
172+
if value.isNaN { return "NaN" }
173+
return value > 0 ? "Infinity" : "-Infinity"
174+
}
175+
156176
/// Format binary data: 16-byte values as UUID, otherwise as hex string
157177
private static func formatBinaryData(_ data: Data) -> String {
158178
if data.count == 16 {
@@ -193,27 +213,16 @@ struct BsonDocumentFlattener {
193213

194214
switch value {
195215
case let num as NSNumber:
196-
if CFBooleanGetTypeID() == CFGetTypeID(num) {
216+
if isBoolean(num) {
197217
return 8 // Boolean
198218
}
199-
let objCType = String(cString: num.objCType)
200-
if objCType == "d" || objCType == "f" {
219+
if isFloatingPoint(num) {
201220
return 1 // Double
202221
}
203-
if objCType == "q" || objCType == "l" {
204-
return 18 // Int64
205-
}
206-
return 16 // Int32
222+
let objCType = String(cString: num.objCType)
223+
return objCType == "q" || objCType == "l" ? 18 : 16 // Int64 : Int32
207224
case is String:
208225
return 2 // String
209-
case is Bool:
210-
return 8 // Boolean
211-
case is Int, is Int32:
212-
return 16 // Int32
213-
case is Int64:
214-
return 18 // Int64
215-
case is Double, is Float:
216-
return 1 // Double
217226
case is Date:
218227
return 9 // Date
219228
case is Data:

Plugins/MongoDBDriverPlugin/MongoDBPluginDriver.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -908,7 +908,9 @@ final class MongoDBPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
908908
}
909909

910910
private func prettyJson(_ value: Any) -> String {
911-
guard let data = try? JSONSerialization.data(withJSONObject: value, options: [.sortedKeys, .prettyPrinted]),
911+
let sanitized = BsonDocumentFlattener.sanitizeForJson(value)
912+
guard JSONSerialization.isValidJSONObject(sanitized),
913+
let data = try? JSONSerialization.data(withJSONObject: sanitized, options: [.sortedKeys, .prettyPrinted]),
912914
let json = String(data: data, encoding: .utf8) else {
913915
return String(describing: value)
914916
}

0 commit comments

Comments
 (0)