Skip to content

Commit 40bd5eb

Browse files
committed
Refactored Number Coding in FunctionsSerializer
* Introduced `WrappedNumber` as a type-safe wrapper for numeric values which have to be encoded / decoded to fit JS limits * Since numbers other than big integers don’t need special encoding, it’s not necessary to process them separately for each type—all numbers except `q` and `Q` are returned as is (just as before) * For decoding, wrapped numbers are parsed using Swift types `Int` and `UInt` * These changes further reduce the usage of Objective-C types, and make it easier to (de)serialize Functions payloads using `Codable` in the future
1 parent 4f6c342 commit 40bd5eb

File tree

2 files changed

+66
-84
lines changed

2 files changed

+66
-84
lines changed

FirebaseFunctions/Sources/Internal/FunctionsSerializer.swift

Lines changed: 60 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(WrappedNumber)
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,70 @@ 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(wrapped)
113+
}
114+
return n
115+
case .unsignedLong:
116+
guard let n = UInt(wrapped.value) else {
117+
throw .failedToParseWrappedNumber(wrapped)
161118
}
162119
return n
120+
}
121+
}
122+
}
123+
124+
// MARK: - WrappedNumber
125+
126+
extension FunctionsSerializer {
127+
struct WrappedNumber {
128+
let type: NumberType
129+
let value: String
130+
131+
// When / if objects are encoded / decoded using `Codable`,
132+
// these two `init`s and `encoded` won’t be needed anymore:
133+
134+
init(type: NumberType, value: String) {
135+
self.type = type
136+
self.value = value
137+
}
163138

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)
139+
init?(from dictionary: NSDictionary) {
140+
guard
141+
let typeString = dictionary["@type"] as? String,
142+
let type = NumberType(rawValue: typeString),
143+
let value = dictionary["value"] as? String
144+
else {
145+
return nil
171146
}
172-
return NSNumber(value: returnValue)
173147

174-
default:
175-
return nil
148+
self.init(type: type, value: value)
149+
}
150+
151+
var encoded: [String: String] {
152+
["@type": type.rawValue, "value": value]
153+
}
154+
155+
enum NumberType: String {
156+
case long = "type.googleapis.com/google.protobuf.Int64Value"
157+
case unsignedLong = "type.googleapis.com/google.protobuf.UInt64Value"
176158
}
177159
}
178160
}

FirebaseFunctions/Tests/Unit/FunctionsSerializerTests.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,9 @@ 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) {
102-
XCTAssertEqual(value, badVal)
103-
XCTAssertEqual(type, typeString)
101+
} catch let FunctionsSerializer.Error.failedToParseWrappedNumber(wrapped) {
102+
XCTAssertEqual(wrapped.value, badVal)
103+
XCTAssertEqual(wrapped.type.rawValue, typeString)
104104
return
105105
}
106106
XCTFail()
@@ -136,9 +136,9 @@ 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) {
140-
XCTAssertEqual(value, tooHighVal)
141-
XCTAssertEqual(type, typeString)
139+
} catch let FunctionsSerializer.Error.failedToParseWrappedNumber(wrapped) {
140+
XCTAssertEqual(wrapped.value, tooHighVal)
141+
XCTAssertEqual(wrapped.type.rawValue, typeString)
142142
return
143143
}
144144
XCTFail()

0 commit comments

Comments
 (0)