Skip to content

Commit 173e7d8

Browse files
authored
fix: Improve ParseObject conformance to Hashable (#176)
* fix: Improve ParseObject conformance to Hashable * nit * another nit * Test ParseError hashing * Use compiler level equatable for ParseFile * fix file test * improve identifiable documentation
1 parent b3e84a2 commit 173e7d8

13 files changed

+76
-64
lines changed

CHANGELOG.md

+8-2
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,17 @@
22
# Parse-Swift Changelog
33

44
### main
5-
[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.10.1...main), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/main/documentation/parseswift)
5+
[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.10.2...main), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/main/documentation/parseswift)
66
* _Contributing to this repo? Add info about your change here to be included in the next release_
77

8+
### 5.10.2
9+
[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.10.1...5.10.2), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/5.10.2/documentation/parseswift)
10+
11+
__Fixes__
12+
* Improve ParseObject conformance to Hashable to prevent collision attacks ([#176](https://github.com/netreconlab/Parse-Swift/pull/176)), thanks to [Corey Baker](https://github.com/cbaker6).
13+
814
### 5.10.1
9-
[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.10.0...5.10.1), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/5.10.0/documentation/parseswift)
15+
[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.10.0...5.10.1), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/5.10.1/documentation/parseswift)
1016

1117
__Fixes__
1218
* Make ParseEncoder sendable ([#175](https://github.com/netreconlab/Parse-Swift/pull/175)), thanks to [Corey Baker](https://github.com/cbaker6).

Sources/ParseSwift/Objects/ParseObject.swift

+10-15
Original file line numberDiff line numberDiff line change
@@ -172,16 +172,6 @@ public protocol ParseObject: ParseTypeable,
172172
// MARK: Default Implementations
173173
public extension ParseObject {
174174

175-
/**
176-
A computed property that is a unique identifier and makes it easy to use `ParseObject`'s
177-
as models in MVVM and SwiftUI.
178-
- note: `id` allows `ParseObject`'s to be used even if they have not been saved and/or missing an `objectId`.
179-
- important: `id` will have the same value as `objectId` when a `ParseObject` contains an `objectId`.
180-
*/
181-
var id: String {
182-
objectId ?? UUID().uuidString
183-
}
184-
185175
var mergeable: Self {
186176
guard isSaved,
187177
originalData == nil else {
@@ -245,13 +235,18 @@ extension ParseObject {
245235
}
246236
}
247237

248-
// MARK: Hashable
238+
// MARK: Identifiable
249239
public extension ParseObject {
250-
func hash(into hasher: inout Hasher) {
251-
hasher.combine(self.id)
252-
hasher.combine(createdAt)
253-
hasher.combine(updatedAt)
240+
241+
/**
242+
A computed property that ensures `ParseObject`'s can be uniquely identified across instances.
243+
- note: `id` allows `ParseObject`'s to be uniquely identified even if they have not been saved and/or missing an `objectId`.
244+
- important: `id` will have the same value as `objectId` when a `ParseObject` contains an `objectId`.
245+
*/
246+
var id: String {
247+
objectId ?? UUID().uuidString
254248
}
249+
255250
}
256251

257252
// MARK: Helper Methods

Sources/ParseSwift/Objects/ParseRole.swift

-7
Original file line numberDiff line numberDiff line change
@@ -104,13 +104,6 @@ public extension ParseRole {
104104
self.ACL = acl
105105
}
106106

107-
func hash(into hasher: inout Hasher) {
108-
let name = self.name ?? self.objectId
109-
hasher.combine(name)
110-
hasher.combine(createdAt)
111-
hasher.combine(updatedAt)
112-
}
113-
114107
func mergeParse(with object: Self) throws -> Self {
115108
guard hasSameObjectId(as: object) else {
116109
throw ParseError(code: .otherCause,

Sources/ParseSwift/ParseConstants.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Foundation
1010

1111
enum ParseConstants {
1212
static let sdk = "swift"
13-
static let version = "5.10.1"
13+
static let version = "5.10.2"
1414
static let fileManagementDirectory = "parse/"
1515
static let fileManagementPrivateDocumentsDirectory = "Private Documents/"
1616
static let fileManagementLibraryDirectory = "Library/"

Sources/ParseSwift/Protocols/Fileable.swift

-8
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,4 @@ extension Fileable {
1818
var isSaved: Bool {
1919
return url != nil
2020
}
21-
22-
public static func == (lhs: Self, rhs: Self) -> Bool {
23-
guard let lURL = lhs.url,
24-
let rURL = rhs.url else {
25-
return lhs.id == rhs.id
26-
}
27-
return lURL == rURL
28-
}
2921
}

Sources/ParseSwift/Protocols/ParseHookParametable.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ import Foundation
1212
Conforming to `ParseHookParametable` allows types that can be created
1313
to decode parameters in `ParseHookFunctionRequest`'s.
1414
*/
15-
public protocol ParseHookParametable: Codable, Equatable, Sendable {}
15+
public protocol ParseHookParametable: ParseTypeable {}

Sources/ParseSwift/Protocols/ParseTypeable.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import Foundation
1313
*/
1414
public protocol ParseTypeable: Codable,
1515
Sendable,
16-
Equatable,
16+
Hashable,
1717
CustomDebugStringConvertible,
1818
CustomStringConvertible {}
1919

Sources/ParseSwift/Types/ParseError.swift

+11
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,17 @@ extension ParseError: LocalizedError {
540540
}
541541
}
542542

543+
// MARK: Hashable
544+
extension ParseError {
545+
public func hash(into hasher: inout Hasher) {
546+
hasher.combine(code)
547+
hasher.combine(message)
548+
hasher.combine(error)
549+
hasher.combine(otherCode)
550+
hasher.combine(swift?.localizedDescription)
551+
}
552+
}
553+
543554
// MARK: Equatable
544555
extension ParseError: Equatable {
545556
public static func == (lhs: Self, rhs: Self) -> Bool {

Sources/ParseSwift/Types/ParseFile.swift

+24-26
Original file line numberDiff line numberDiff line change
@@ -22,28 +22,6 @@ public struct ParseFile: Fileable, Savable, Deletable, Hashable, Identifiable {
2222
&& data == nil
2323
}
2424

25-
/**
26-
A computed property that is a unique identifier and makes it easy to use `ParseFile`'s
27-
as models in MVVM and SwiftUI.
28-
- note: `id` allows `ParseFile`'s to be used even when they are not saved.
29-
- important: `id` will have the same value as `name` when a `ParseFile` is saved.
30-
*/
31-
public var id: String {
32-
guard isSaved else {
33-
guard let cloudURL = cloudURL else {
34-
guard let localURL = localURL else {
35-
guard let data = data else {
36-
return name
37-
}
38-
return "\(name)_\(data)"
39-
}
40-
return combineName(with: localURL)
41-
}
42-
return combineName(with: cloudURL)
43-
}
44-
return name
45-
}
46-
4725
/**
4826
The name of the file.
4927
Before the file is saved, this is the filename given by the user.
@@ -159,10 +137,6 @@ public struct ParseFile: Fileable, Savable, Deletable, Hashable, Identifiable {
159137
self.options = options
160138
}
161139

162-
public func hash(into hasher: inout Hasher) {
163-
hasher.combine(self.id)
164-
}
165-
166140
public func isSaved() async throws -> Bool {
167141
isSaved
168142
}
@@ -174,6 +148,30 @@ public struct ParseFile: Fileable, Savable, Deletable, Hashable, Identifiable {
174148
}
175149
}
176150

151+
// MARK: Identifiable
152+
extension ParseFile {
153+
/**
154+
A computed property that ensures `ParseFile`'s can be uniquely identified across instances.
155+
- note: `id` allows `ParseFile`'s to be uniquely identified even if they have not been saved.
156+
- important: `id` will have the same value as `objectId` when a `ParseObject` contains an `objectId`.
157+
*/
158+
public var id: String {
159+
guard isSaved else {
160+
guard let cloudURL = cloudURL else {
161+
guard let localURL = localURL else {
162+
guard let data = data else {
163+
return name
164+
}
165+
return "\(name)_\(data)"
166+
}
167+
return combineName(with: localURL)
168+
}
169+
return combineName(with: cloudURL)
170+
}
171+
return name
172+
}
173+
}
174+
177175
// MARK: Helper Methods (internal)
178176
extension ParseFile {
179177
func combineName(with url: URL) -> String {

Sources/ParseSwift/Types/ParseHookResponse.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import Foundation
1212
Build a response after processing a `ParseHookFunctionRequest`
1313
or `ParseHookTriggerRequest`.
1414
*/
15-
public struct ParseHookResponse<R: Codable & Equatable & Sendable>: ParseTypeable {
15+
public struct ParseHookResponse<R: Codable & Hashable & Sendable>: ParseTypeable {
1616
/// The data to return in the response.
1717
public var success: R?
1818
/// An object with a Parse code and message.

Sources/ParseSwift/Types/Query.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ public struct Query<T>: ParseTypeable where T: ParseObject {
132132

133133
- parameter key: The key to order by.
134134
*/
135-
public enum Order: Codable, Equatable, Sendable {
135+
public enum Order: ParseTypeable {
136136
/// Sort in ascending order based on `key`.
137137
case ascending(String)
138138
/// Sort in descending order based on `key`.

Tests/ParseSwiftTests/ParseErrorTests.swift

+17
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,23 @@ class ParseErrorTests: XCTestCase {
159159
XCTAssertNil(error.containedIn([.operationForbidden, .invalidQuery]))
160160
}
161161

162+
func testHashing() throws {
163+
let error1 = ParseError(code: .accountAlreadyLinked, message: "Hello")
164+
let error2 = ParseError(code: .accountAlreadyLinked, message: "World")
165+
let error3 = error1
166+
167+
var setOfSameErrors = Set([error1, error1, error3])
168+
XCTAssertEqual(setOfSameErrors.count, 1)
169+
XCTAssertEqual(setOfSameErrors.first, error1)
170+
XCTAssertEqual(setOfSameErrors.first, error3)
171+
XCTAssertNotEqual(setOfSameErrors.first, error2)
172+
setOfSameErrors.insert(error2)
173+
XCTAssertEqual(setOfSameErrors.count, 2)
174+
XCTAssertTrue(setOfSameErrors.contains(error1))
175+
XCTAssertTrue(setOfSameErrors.contains(error2))
176+
XCTAssertTrue(setOfSameErrors.contains(error3))
177+
}
178+
162179
func testErrorCount() throws {
163180
let errorCodes = ParseError.Code.allCases
164181
XCTAssertGreaterThan(errorCodes.count, 50)

Tests/ParseSwiftTests/ParseObjectAsyncTests.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -1801,7 +1801,7 @@ class ParseObjectAsyncTests: XCTestCase { // swiftlint:disable:this type_body_le
18011801
XCTAssertEqual(savedGame.objectId, gameOnServer.objectId)
18021802
XCTAssertEqual(savedGame.createdAt, gameOnServer.createdAt)
18031803
XCTAssertEqual(savedGame.updatedAt, gameOnServer.createdAt)
1804-
XCTAssertEqual(savedGame.profilePicture, gameOnServer.profilePicture)
1804+
XCTAssertEqual(savedGame.profilePicture?.url, gameOnServer.profilePicture?.url)
18051805
}
18061806
#endif
18071807
}

0 commit comments

Comments
 (0)