Skip to content

Commit 0004edb

Browse files
authored
Refactored Number Coding in FunctionsSerializer (#14889)
1 parent b6eacbf commit 0004edb

File tree

2 files changed

+100
-80
lines changed

2 files changed

+100
-80
lines changed

FirebaseFunctions/Sources/Internal/FunctionsSerializer.swift

Lines changed: 66 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,10 @@
1414

1515
import Foundation
1616

17-
private enum Constants {
18-
static let longType = "type.googleapis.com/google.protobuf.Int64Value"
19-
static let unsignedLongType = "type.googleapis.com/google.protobuf.UInt64Value"
20-
static let dateType = "type.googleapis.com/google.protobuf.Timestamp"
21-
}
22-
2317
extension FunctionsSerializer {
2418
enum Error: Swift.Error {
2519
case unsupportedType(typeName: String)
26-
case unknownNumberType(charValue: String, number: NSNumber)
27-
case invalidValueForType(value: String, requestedType: String)
20+
case failedToParseWrappedNumber(value: String, type: String)
2821
}
2922
}
3023

@@ -41,8 +34,8 @@ final class FunctionsSerializer: Sendable {
4134
func encode(_ object: Any) throws -> Any {
4235
if object is NSNull {
4336
return object
44-
} else if object is NSNumber {
45-
return try encodeNumber(object as! NSNumber)
37+
} else if let number = object as? NSNumber {
38+
return wrapNumberIfNeeded(number)
4639
} else if object is NSString {
4740
return object
4841
} else if let dict = object as? NSDictionary {
@@ -70,16 +63,8 @@ final class FunctionsSerializer: Sendable {
7063
func decode(_ object: Any) throws -> Any {
7164
// Return these types as is. PORTING NOTE: Moved from the bottom of the func for readability.
7265
if let dict = object as? NSDictionary {
73-
if let requestedType = dict["@type"] as? String {
74-
guard let value = dict["value"] as? String else {
75-
// Seems like we should throw here - but this maintains compatibility.
76-
return dict
77-
}
78-
if let result = try decodeWrappedType(requestedType, value) {
79-
return result
80-
}
81-
82-
// Treat unknown types as dictionaries, so we don't crash old clients when we add types.
66+
if let wrappedNumber = WrappedNumber(from: dict) {
67+
return try unwrapNumber(wrappedNumber)
8368
}
8469

8570
let decoded = NSMutableDictionary()
@@ -106,73 +91,76 @@ final class FunctionsSerializer: Sendable {
10691
String(describing: type(of: value))
10792
}
10893

109-
private func encodeNumber(_ number: NSNumber) throws -> AnyObject {
110-
// Recover the underlying type of the number, using the method described here:
111-
// http://stackoverflow.com/questions/2518761/get-type-of-nsnumber
112-
let cType = number.objCType
113-
114-
// Type Encoding values taken from
115-
// https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/
116-
// Articles/ocrtTypeEncodings.html
117-
switch cType[0] {
118-
case CChar("q".utf8.first!):
119-
// "long long" might be larger than JS supports, so make it a string.
120-
return ["@type": Constants.longType, "value": "\(number)"] as AnyObject
121-
122-
case CChar("Q".utf8.first!):
123-
// "unsigned long long" might be larger than JS supports, so make it a string.
124-
return ["@type": Constants.unsignedLongType,
125-
"value": "\(number)"] as AnyObject
126-
127-
case CChar("i".utf8.first!),
128-
CChar("s".utf8.first!),
129-
CChar("l".utf8.first!),
130-
CChar("I".utf8.first!),
131-
CChar("S".utf8.first!):
132-
// If it"s an integer that isn"t too long, so just use the number.
133-
return number
134-
135-
case CChar("f".utf8.first!), CChar("d".utf8.first!):
136-
// It"s a float/double that"s not too large.
137-
return number
138-
139-
case CChar("B".utf8.first!), CChar("c".utf8.first!), CChar("C".utf8.first!):
140-
// Boolean values are weird.
141-
//
142-
// On arm64, objCType of a BOOL-valued NSNumber will be "c", even though @encode(BOOL)
143-
// returns "B". "c" is the same as @encode(signed char). Unfortunately this means that
144-
// legitimate usage of signed chars is impossible, but this should be rare.
145-
//
146-
// Just return Boolean values as-is.
147-
return number
148-
94+
private func wrapNumberIfNeeded(_ number: NSNumber) -> Any {
95+
switch String(cString: number.objCType) {
96+
case "q":
97+
// "long long" might be larger than JS supports, so make it a string:
98+
return WrappedNumber(type: .long, value: "\(number)").encoded
99+
case "Q":
100+
// "unsigned long long" might be larger than JS supports, so make it a string:
101+
return WrappedNumber(type: .unsignedLong, value: "\(number)").encoded
149102
default:
150-
// All documented codes should be handled above, so this shouldn"t happen.
151-
throw Error.unknownNumberType(charValue: String(cType[0]), number: number)
103+
// All other types should fit JS limits, so return the number as is:
104+
return number
152105
}
153106
}
154107

155-
private func decodeWrappedType(_ type: String, _ value: String) throws -> AnyObject? {
156-
switch type {
157-
case Constants.longType:
158-
let formatter = NumberFormatter()
159-
guard let n = formatter.number(from: value) else {
160-
throw Error.invalidValueForType(value: value, requestedType: type)
108+
private func unwrapNumber(_ wrapped: WrappedNumber) throws(Error) -> any Numeric {
109+
switch wrapped.type {
110+
case .long:
111+
guard let n = Int(wrapped.value) else {
112+
throw .failedToParseWrappedNumber(
113+
value: wrapped.value,
114+
type: wrapped.type.rawValue
115+
)
116+
}
117+
return n
118+
case .unsignedLong:
119+
guard let n = UInt(wrapped.value) else {
120+
throw .failedToParseWrappedNumber(
121+
value: wrapped.value,
122+
type: wrapped.type.rawValue
123+
)
161124
}
162125
return n
126+
}
127+
}
128+
}
129+
130+
// MARK: - WrappedNumber
131+
132+
extension FunctionsSerializer {
133+
private struct WrappedNumber {
134+
let type: NumberType
135+
let value: String
136+
137+
// When / if objects are encoded / decoded using `Codable`,
138+
// these two `init`s and `encoded` won’t be needed anymore:
139+
140+
init(type: NumberType, value: String) {
141+
self.type = type
142+
self.value = value
143+
}
163144

164-
case Constants.unsignedLongType:
165-
// NSNumber formatter doesn't handle unsigned long long, so we have to parse it.
166-
let str = (value as NSString).utf8String
167-
var endPtr: UnsafeMutablePointer<CChar>?
168-
let returnValue = UInt64(strtoul(str, &endPtr, 10))
169-
guard String(returnValue) == value else {
170-
throw Error.invalidValueForType(value: value, requestedType: type)
145+
init?(from dictionary: NSDictionary) {
146+
guard
147+
let typeString = dictionary["@type"] as? String,
148+
let type = NumberType(rawValue: typeString),
149+
let value = dictionary["value"] as? String
150+
else {
151+
return nil
171152
}
172-
return NSNumber(value: returnValue)
173153

174-
default:
175-
return nil
154+
self.init(type: type, value: value)
155+
}
156+
157+
var encoded: [String: String] {
158+
["@type": type.rawValue, "value": value]
159+
}
160+
161+
enum NumberType: String {
162+
case long = "type.googleapis.com/google.protobuf.Int64Value"
163+
case unsignedLong = "type.googleapis.com/google.protobuf.UInt64Value"
176164
}
177165
}
178166
}

FirebaseFunctions/Tests/Unit/FunctionsSerializerTests.swift

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ class FunctionsSerializerTests: XCTestCase {
9898
let dictLowLong = ["@type": typeString, "value": badVal]
9999
do {
100100
_ = try serializer.decode(dictLowLong) as? NSNumber
101-
} catch let FunctionsSerializer.Error.invalidValueForType(value, type) {
101+
} catch let FunctionsSerializer.Error.failedToParseWrappedNumber(value, type) {
102102
XCTAssertEqual(value, badVal)
103103
XCTAssertEqual(type, typeString)
104104
return
@@ -136,7 +136,7 @@ class FunctionsSerializerTests: XCTestCase {
136136
let coded = ["@type": typeString, "value": tooHighVal]
137137
do {
138138
_ = try serializer.decode(coded) as? NSNumber
139-
} catch let FunctionsSerializer.Error.invalidValueForType(value, type) {
139+
} catch let FunctionsSerializer.Error.failedToParseWrappedNumber(value, type) {
140140
XCTAssertEqual(value, tooHighVal)
141141
XCTAssertEqual(type, typeString)
142142
return
@@ -283,6 +283,38 @@ class FunctionsSerializerTests: XCTestCase {
283283

284284
try assert(serializer.decode(input), throwsUnsupportedTypeErrorWithName: "CustomObject")
285285
}
286+
287+
// If the object can be decoded as a wrapped number, all other properties are ignored:
288+
func testDecodeValidWrappedNumberWithUnsupportedExtra() throws {
289+
let input = [
290+
"@type": "type.googleapis.com/google.protobuf.Int64Value",
291+
"value": "1234567890",
292+
"extra": CustomObject(),
293+
] as NSDictionary
294+
295+
XCTAssertEqual(NSNumber(1_234_567_890), try serializer.decode(input) as? NSNumber)
296+
}
297+
298+
// If the object is not a valid wrapped number, it’s processed as a generic array:
299+
func testDecodeWrappedNumberWithUnsupportedValue() throws {
300+
let input = [
301+
"@type": "type.googleapis.com/google.protobuf.Int64Value",
302+
"value": CustomObject(),
303+
] as NSDictionary
304+
305+
try assert(serializer.decode(input), throwsUnsupportedTypeErrorWithName: "CustomObject")
306+
}
307+
308+
// If the object is not a valid wrapped number, it’s processed as a generic array:
309+
func testDecodeInvalidWrappedNumberWithUnsupportedExtra() throws {
310+
let input = [
311+
"@type": "CUSTOM_TYPE",
312+
"value": "1234567890",
313+
"extra": CustomObject(),
314+
] as NSDictionary
315+
316+
try assert(serializer.decode(input), throwsUnsupportedTypeErrorWithName: "CustomObject")
317+
}
286318
}
287319

288320
// MARK: - Utilities

0 commit comments

Comments
 (0)