Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- 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)
- MongoDB: opening a collection no longer crashes when a document contains a NaN or infinite number. (#1418)

## [0.45.0] - 2026-05-26

Expand Down
105 changes: 57 additions & 48 deletions Plugins/MongoDBDriverPlugin/BsonDocumentFlattener.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,23 +73,9 @@ struct BsonDocumentFlattener {
case let str as String:
return str
case let num as NSNumber:
// Check if it's a boolean (NSNumber wraps booleans too)
if CFBooleanGetTypeID() == CFGetTypeID(num) {
return num.boolValue ? "true" : "false"
}
return num.stringValue
case let int as Int:
return String(int)
case let int32 as Int32:
return String(int32)
case let int64 as Int64:
return String(int64)
case let double as Double:
return String(double)
case let bool as Bool:
return bool ? "true" : "false"
return displayString(for: num)
case let date as Date:
return ISO8601DateFormatter().string(from: date)
return iso8601Formatter.string(from: date)
case let data as Data:
return formatBinaryData(data)
case let dict as [String: Any]:
Expand Down Expand Up @@ -121,24 +107,20 @@ struct BsonDocumentFlattener {
/// Serialize a dictionary or array to compact JSON string
static func serializeToJson(_ value: Any) -> String {
let sanitized = sanitizeForJson(value)
do {
let data = try JSONSerialization.data(withJSONObject: sanitized, options: [.sortedKeys])
if let json = String(data: data, encoding: .utf8) {
// Cap at 10k chars to prevent mega-document display issues
let nsJson = json as NSString
if nsJson.length > 10_000 {
return String(json.prefix(10_000)) + "..."
}
return json
}
} catch {
// Fall through to description
guard JSONSerialization.isValidJSONObject(sanitized),
let data = try? JSONSerialization.data(withJSONObject: sanitized, options: [.sortedKeys]),
let json = String(data: data, encoding: .utf8) else {
return String(describing: value)
}
return String(describing: value)
let nsJson = json as NSString
if nsJson.length > 10_000 {
return String(json.prefix(10_000)) + "..."
}
return json
}

/// Recursively convert non-JSON-safe types (Data, Date, etc.) to JSON-safe representations
private static func sanitizeForJson(_ value: Any) -> Any {
/// Recursively convert every value into a JSON-safe representation
static func sanitizeForJson(_ value: Any) -> Any {
switch value {
case let dict as [String: Any]:
return dict.mapValues { sanitizeForJson($0) }
Expand All @@ -147,12 +129,50 @@ struct BsonDocumentFlattener {
case let data as Data:
return formatBinaryData(data)
case let date as Date:
return ISO8601DateFormatter().string(from: date)
default:
return iso8601Formatter.string(from: date)
case is NSNull:
return value
case let str as String:
return str
case let num as NSNumber:
return sanitizeNumber(num)
default:
return String(describing: value)
}
}

private static let iso8601Formatter = ISO8601DateFormatter()

private static func displayString(for num: NSNumber) -> String {
if isBoolean(num) {
return num.boolValue ? "true" : "false"
}
if isFloatingPoint(num), !num.doubleValue.isFinite {
return nonFiniteToken(num.doubleValue)
}
return num.stringValue
}

private static func sanitizeNumber(_ num: NSNumber) -> Any {
guard !isBoolean(num) else { return num }
guard isFloatingPoint(num), !num.doubleValue.isFinite else { return num }
return nonFiniteToken(num.doubleValue)
}

private static func isBoolean(_ num: NSNumber) -> Bool {
CFBooleanGetTypeID() == CFGetTypeID(num)
}

private static func isFloatingPoint(_ num: NSNumber) -> Bool {
let objCType = String(cString: num.objCType)
return objCType == "d" || objCType == "f"
}

private static func nonFiniteToken(_ value: Double) -> String {
if value.isNaN { return "NaN" }
return value > 0 ? "Infinity" : "-Infinity"
}

/// Format binary data: 16-byte values as UUID, otherwise as hex string
private static func formatBinaryData(_ data: Data) -> String {
if data.count == 16 {
Expand Down Expand Up @@ -193,27 +213,16 @@ struct BsonDocumentFlattener {

switch value {
case let num as NSNumber:
if CFBooleanGetTypeID() == CFGetTypeID(num) {
if isBoolean(num) {
return 8 // Boolean
}
let objCType = String(cString: num.objCType)
if objCType == "d" || objCType == "f" {
if isFloatingPoint(num) {
return 1 // Double
}
if objCType == "q" || objCType == "l" {
return 18 // Int64
}
return 16 // Int32
let objCType = String(cString: num.objCType)
return objCType == "q" || objCType == "l" ? 18 : 16 // Int64 : Int32
case is String:
return 2 // String
case is Bool:
return 8 // Boolean
case is Int, is Int32:
return 16 // Int32
case is Int64:
return 18 // Int64
case is Double, is Float:
return 1 // Double
case is Date:
return 9 // Date
case is Data:
Expand Down
4 changes: 3 additions & 1 deletion Plugins/MongoDBDriverPlugin/MongoDBPluginDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -908,7 +908,9 @@ final class MongoDBPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
}

private func prettyJson(_ value: Any) -> String {
guard let data = try? JSONSerialization.data(withJSONObject: value, options: [.sortedKeys, .prettyPrinted]),
let sanitized = BsonDocumentFlattener.sanitizeForJson(value)
guard JSONSerialization.isValidJSONObject(sanitized),
let data = try? JSONSerialization.data(withJSONObject: sanitized, options: [.sortedKeys, .prettyPrinted]),
let json = String(data: data, encoding: .utf8) else {
return String(describing: value)
}
Expand Down
Loading
Loading