Skip to content

Commit ddc6d06

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 * 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 ddc6d06

File tree

2 files changed

+67
-84
lines changed

2 files changed

+67
-84
lines changed

FirebaseFunctions/Sources/Internal/FunctionsSerializer.swift

Lines changed: 61 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,9 @@ 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+
let number = try unwrapNumber(wrappedNumber) {
68+
return number
8369
}
8470

8571
let decoded = NSMutableDictionary()
@@ -106,73 +92,70 @@ final class FunctionsSerializer: Sendable {
10692
String(describing: type(of: value))
10793
}
10894

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-
95+
private func wrapNumberIfNeeded(_ number: NSNumber) -> Any {
96+
switch String(cString: number.objCType) {
97+
case "q":
98+
// "long long" might be larger than JS supports, so make it a string:
99+
return WrappedNumber(type: .long, value: "\(number)").encoded
100+
case "Q":
101+
// "unsigned long long" might be larger than JS supports, so make it a string:
102+
return WrappedNumber(type: .unsignedLong, value: "\(number)").encoded
149103
default:
150-
// All documented codes should be handled above, so this shouldn"t happen.
151-
throw Error.unknownNumberType(charValue: String(cType[0]), number: number)
104+
// All other types should fit JS limits, so return the number as is:
105+
return number
152106
}
153107
}
154108

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)
109+
private func unwrapNumber(_ wrapped: WrappedNumber) throws(Error) -> (any Numeric)? {
110+
switch wrapped.type {
111+
case .long:
112+
guard let n = Int(wrapped.value) else {
113+
throw .failedToParseWrappedNumber(wrapped)
114+
}
115+
return n
116+
case .unsignedLong:
117+
guard let n = UInt(wrapped.value) else {
118+
throw .failedToParseWrappedNumber(wrapped)
161119
}
162120
return n
121+
}
122+
}
123+
}
124+
125+
// MARK: - WrappedNumber
126+
127+
extension FunctionsSerializer {
128+
struct WrappedNumber {
129+
let type: NumberType
130+
let value: String
131+
132+
// When / if objects are encoded / decoded using `Codable`,
133+
// these two `init`s and `encoded` won’t be needed anymore:
134+
135+
init(type: NumberType, value: String) {
136+
self.type = type
137+
self.value = value
138+
}
163139

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

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

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)