-
Notifications
You must be signed in to change notification settings - Fork 24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
SWIFT-1126 / SWIFT-1137 Add legacy extended JSON parsing, fix Date-related crashes #64
Changes from all commits
54a2fa4
b32c8de
3126927
07204c2
9857de7
6eebce1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -65,6 +65,7 @@ public struct BSONRegularExpression: Equatable, Hashable { | |
|
||
extension BSONRegularExpression: BSONValue { | ||
internal static let extJSONTypeWrapperKeys: [String] = ["$regularExpression"] | ||
internal static let extJSONLegacyTypeWrapperKeys: [String] = ["$regex", "$options"] | ||
|
||
/* | ||
* Initializes a `BSONRegularExpression` from ExtendedJSON. | ||
|
@@ -81,22 +82,36 @@ extension BSONRegularExpression: BSONValue { | |
* - `DecodingError` if `json` is a partial match or is malformed. | ||
*/ | ||
internal init?(fromExtJSON json: JSON, keyPath: [String]) throws { | ||
// canonical and relaxed extended JSON | ||
guard let value = try json.value.unwrapObject(withKey: "$regularExpression", keyPath: keyPath) else { | ||
return nil | ||
} | ||
guard | ||
let (pattern, options) = try value.unwrapObject(withKeys: "pattern", "options", keyPath: keyPath), | ||
let patternStr = pattern.stringValue, | ||
let optionsStr = options.stringValue | ||
else { | ||
throw DecodingError._extendedJSONError( | ||
keyPath: keyPath, | ||
debugDescription: "Could not parse `BSONRegularExpression` from \"\(value)\", " + | ||
"\"pattern\" and \"options\" must be strings" | ||
) | ||
// canonical and relaxed extended JSON v2 | ||
if let regex = try json.value.unwrapObject(withKey: "$regularExpression", keyPath: keyPath) { | ||
guard | ||
let (pattern, options) = try regex.unwrapObject(withKeys: "pattern", "options", keyPath: keyPath), | ||
let patternStr = pattern.stringValue, | ||
let optionsStr = options.stringValue | ||
else { | ||
throw DecodingError._extendedJSONError( | ||
keyPath: keyPath, | ||
debugDescription: "Could not parse `BSONRegularExpression` from \"\(regex)\", " + | ||
"\"pattern\" and \"options\" must be strings" | ||
) | ||
} | ||
self = BSONRegularExpression(pattern: patternStr, options: optionsStr) | ||
return | ||
} else { | ||
// legacy / v1 extended JSON | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this parses the following format:
which is unfortunately, the exact same format as a query operator. |
||
guard | ||
let (pattern, options) = try? json.value.unwrapObject(withKeys: "$regex", "$options", keyPath: keyPath), | ||
let patternStr = pattern.stringValue, | ||
let optionsStr = options.stringValue | ||
else { | ||
// instead of a throwing an error here or as part of unwrapObject, we just return nil to avoid erroring | ||
// when a $regex query operator is being parsed from extended JSON. See the | ||
// "Regular expression as value of $regex query operator with $options" corpus test. | ||
return nil | ||
} | ||
self = BSONRegularExpression(pattern: patternStr, options: optionsStr) | ||
return | ||
} | ||
self = BSONRegularExpression(pattern: patternStr, options: optionsStr) | ||
} | ||
|
||
/// Converts this `BSONRegularExpression` to a corresponding `JSON` in relaxed extendedJSON format. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,9 @@ import NIO | |
extension Date: BSONValue { | ||
internal static let extJSONTypeWrapperKeys: [String] = ["$date"] | ||
|
||
/// The range of datetimes that can be represented in BSON. | ||
private static let VALID_BSON_DATES: Range<Date> = Date(msSinceEpoch: Int64.min)..<Date(msSinceEpoch: Int64.max) | ||
|
||
/* | ||
* Initializes a `Date` from ExtendedJSON. | ||
* | ||
|
@@ -48,6 +51,15 @@ extension Date: BSONValue { | |
) | ||
} | ||
self = date | ||
case let .number(ms): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. legacy allows for a regular number instead of a |
||
// legacy extended JSON | ||
guard let msInt64 = Int64(ms) else { | ||
throw DecodingError._extendedJSONError( | ||
keyPath: keyPath, | ||
debugDescription: "Expected \(ms) to be valid Int64 representing milliseconds since epoch" | ||
) | ||
} | ||
self = Date(msSinceEpoch: msInt64) | ||
default: | ||
throw DecodingError._extendedJSONError( | ||
keyPath: keyPath, | ||
|
@@ -87,7 +99,20 @@ extension Date: BSONValue { | |
internal var bson: BSON { .datetime(self) } | ||
|
||
/// The number of milliseconds after the Unix epoch that this `Date` occurs. | ||
internal var msSinceEpoch: Int64 { Int64((self.timeIntervalSince1970 * 1000.0).rounded()) } | ||
/// If the date is further in the future than Int64.max milliseconds from the epoch, | ||
/// Int64.max is returned to prevent a crash. | ||
internal var msSinceEpoch: Int64 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. perhaps we should mention this behavior somewhere more user-facing, maybe on the
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good idea, done |
||
// to prevent the application from crashing, we simply clamp the date to the range representable | ||
// by an Int64 ms since epoch | ||
guard self > Self.VALID_BSON_DATES.lowerBound else { | ||
return Int64.min | ||
} | ||
guard self < Self.VALID_BSON_DATES.upperBound else { | ||
return Int64.max | ||
} | ||
|
||
return Int64((self.timeIntervalSince1970 * 1000.0).rounded()) | ||
} | ||
|
||
/// Initializes a new `Date` representing the instance `msSinceEpoch` milliseconds | ||
/// since the Unix epoch. | ||
|
@@ -105,4 +130,8 @@ extension Date: BSONValue { | |
internal func write(to buffer: inout ByteBuffer) { | ||
buffer.writeInteger(self.msSinceEpoch, endianness: .little, as: Int64.self) | ||
} | ||
|
||
internal func isValidBSONDate() -> Bool { | ||
Self.VALID_BSON_DATES.contains(self) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,8 +17,15 @@ public class ExtendedJSONDecoder { | |
}() | ||
|
||
/// A set of all the possible extendedJSON wrapper keys. | ||
/// This does not include the legacy extended JSON wrapper keys. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we allow things like "$type" to be in a regular document (e.g. as part of a query operator), the legacy wrapper keys can't be used to determine errors like we do with the reserved ones, so we need to separate them out. |
||
private static var wrapperKeySet: Set<String> = { | ||
Set(ExtendedJSONDecoder.wrapperKeyMap.keys) | ||
var keys: Set<String> = [] | ||
for t in BSON.allBSONTypes.values { | ||
for k in t.extJSONTypeWrapperKeys { | ||
keys.insert(k) | ||
} | ||
} | ||
return keys | ||
}() | ||
|
||
/// A map from extended JSON wrapper keys (e.g. "$numberLong") to the BSON type(s) that they correspond to. | ||
|
@@ -33,6 +40,9 @@ public class ExtendedJSONDecoder { | |
for k in t.extJSONTypeWrapperKeys { | ||
map[k, default: []].append(t.self) | ||
} | ||
for k in t.extJSONLegacyTypeWrapperKeys { | ||
map[k, default: []].append(t.self) | ||
} | ||
} | ||
return map | ||
}() | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this parses the following format: