From 173e7d8d339e9d5f4288c54dc63a088b2062a0d0 Mon Sep 17 00:00:00 2001 From: Corey Date: Sat, 13 Jul 2024 11:56:17 -0700 Subject: [PATCH] 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 --- CHANGELOG.md | 10 +++- Sources/ParseSwift/Objects/ParseObject.swift | 25 ++++------ Sources/ParseSwift/Objects/ParseRole.swift | 7 --- Sources/ParseSwift/ParseConstants.swift | 2 +- Sources/ParseSwift/Protocols/Fileable.swift | 8 --- .../Protocols/ParseHookParametable.swift | 2 +- .../ParseSwift/Protocols/ParseTypeable.swift | 2 +- Sources/ParseSwift/Types/ParseError.swift | 11 ++++ Sources/ParseSwift/Types/ParseFile.swift | 50 +++++++++---------- .../ParseSwift/Types/ParseHookResponse.swift | 2 +- Sources/ParseSwift/Types/Query.swift | 2 +- Tests/ParseSwiftTests/ParseErrorTests.swift | 17 +++++++ .../ParseObjectAsyncTests.swift | 2 +- 13 files changed, 76 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab03652b2..be188c0e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,17 @@ # Parse-Swift Changelog ### main -[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.10.1...main), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/main/documentation/parseswift) +[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.10.2...main), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/main/documentation/parseswift) * _Contributing to this repo? Add info about your change here to be included in the next release_ +### 5.10.2 +[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) + +__Fixes__ +* 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). + ### 5.10.1 -[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) +[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) __Fixes__ * Make ParseEncoder sendable ([#175](https://github.com/netreconlab/Parse-Swift/pull/175)), thanks to [Corey Baker](https://github.com/cbaker6). diff --git a/Sources/ParseSwift/Objects/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift index ccb4e3973..9659993b0 100644 --- a/Sources/ParseSwift/Objects/ParseObject.swift +++ b/Sources/ParseSwift/Objects/ParseObject.swift @@ -172,16 +172,6 @@ public protocol ParseObject: ParseTypeable, // MARK: Default Implementations public extension ParseObject { - /** - A computed property that is a unique identifier and makes it easy to use `ParseObject`'s - as models in MVVM and SwiftUI. - - note: `id` allows `ParseObject`'s to be used even if they have not been saved and/or missing an `objectId`. - - important: `id` will have the same value as `objectId` when a `ParseObject` contains an `objectId`. - */ - var id: String { - objectId ?? UUID().uuidString - } - var mergeable: Self { guard isSaved, originalData == nil else { @@ -245,13 +235,18 @@ extension ParseObject { } } -// MARK: Hashable +// MARK: Identifiable public extension ParseObject { - func hash(into hasher: inout Hasher) { - hasher.combine(self.id) - hasher.combine(createdAt) - hasher.combine(updatedAt) + + /** + A computed property that ensures `ParseObject`'s can be uniquely identified across instances. + - note: `id` allows `ParseObject`'s to be uniquely identified even if they have not been saved and/or missing an `objectId`. + - important: `id` will have the same value as `objectId` when a `ParseObject` contains an `objectId`. + */ + var id: String { + objectId ?? UUID().uuidString } + } // MARK: Helper Methods diff --git a/Sources/ParseSwift/Objects/ParseRole.swift b/Sources/ParseSwift/Objects/ParseRole.swift index fd91a62af..4cc771164 100644 --- a/Sources/ParseSwift/Objects/ParseRole.swift +++ b/Sources/ParseSwift/Objects/ParseRole.swift @@ -104,13 +104,6 @@ public extension ParseRole { self.ACL = acl } - func hash(into hasher: inout Hasher) { - let name = self.name ?? self.objectId - hasher.combine(name) - hasher.combine(createdAt) - hasher.combine(updatedAt) - } - func mergeParse(with object: Self) throws -> Self { guard hasSameObjectId(as: object) else { throw ParseError(code: .otherCause, diff --git a/Sources/ParseSwift/ParseConstants.swift b/Sources/ParseSwift/ParseConstants.swift index 3c4398f19..a26bdf7da 100644 --- a/Sources/ParseSwift/ParseConstants.swift +++ b/Sources/ParseSwift/ParseConstants.swift @@ -10,7 +10,7 @@ import Foundation enum ParseConstants { static let sdk = "swift" - static let version = "5.10.1" + static let version = "5.10.2" static let fileManagementDirectory = "parse/" static let fileManagementPrivateDocumentsDirectory = "Private Documents/" static let fileManagementLibraryDirectory = "Library/" diff --git a/Sources/ParseSwift/Protocols/Fileable.swift b/Sources/ParseSwift/Protocols/Fileable.swift index 156fdb3d8..4a7f6e49e 100644 --- a/Sources/ParseSwift/Protocols/Fileable.swift +++ b/Sources/ParseSwift/Protocols/Fileable.swift @@ -18,12 +18,4 @@ extension Fileable { var isSaved: Bool { return url != nil } - - public static func == (lhs: Self, rhs: Self) -> Bool { - guard let lURL = lhs.url, - let rURL = rhs.url else { - return lhs.id == rhs.id - } - return lURL == rURL - } } diff --git a/Sources/ParseSwift/Protocols/ParseHookParametable.swift b/Sources/ParseSwift/Protocols/ParseHookParametable.swift index 4bb8c146b..65fa8d725 100644 --- a/Sources/ParseSwift/Protocols/ParseHookParametable.swift +++ b/Sources/ParseSwift/Protocols/ParseHookParametable.swift @@ -12,4 +12,4 @@ import Foundation Conforming to `ParseHookParametable` allows types that can be created to decode parameters in `ParseHookFunctionRequest`'s. */ -public protocol ParseHookParametable: Codable, Equatable, Sendable {} +public protocol ParseHookParametable: ParseTypeable {} diff --git a/Sources/ParseSwift/Protocols/ParseTypeable.swift b/Sources/ParseSwift/Protocols/ParseTypeable.swift index 31e3df094..2bf8bf6a9 100644 --- a/Sources/ParseSwift/Protocols/ParseTypeable.swift +++ b/Sources/ParseSwift/Protocols/ParseTypeable.swift @@ -13,7 +13,7 @@ import Foundation */ public protocol ParseTypeable: Codable, Sendable, - Equatable, + Hashable, CustomDebugStringConvertible, CustomStringConvertible {} diff --git a/Sources/ParseSwift/Types/ParseError.swift b/Sources/ParseSwift/Types/ParseError.swift index 5478f9725..ad7c6e728 100644 --- a/Sources/ParseSwift/Types/ParseError.swift +++ b/Sources/ParseSwift/Types/ParseError.swift @@ -540,6 +540,17 @@ extension ParseError: LocalizedError { } } +// MARK: Hashable +extension ParseError { + public func hash(into hasher: inout Hasher) { + hasher.combine(code) + hasher.combine(message) + hasher.combine(error) + hasher.combine(otherCode) + hasher.combine(swift?.localizedDescription) + } +} + // MARK: Equatable extension ParseError: Equatable { public static func == (lhs: Self, rhs: Self) -> Bool { diff --git a/Sources/ParseSwift/Types/ParseFile.swift b/Sources/ParseSwift/Types/ParseFile.swift index c33011109..bc9bb8bb6 100644 --- a/Sources/ParseSwift/Types/ParseFile.swift +++ b/Sources/ParseSwift/Types/ParseFile.swift @@ -22,28 +22,6 @@ public struct ParseFile: Fileable, Savable, Deletable, Hashable, Identifiable { && data == nil } - /** - A computed property that is a unique identifier and makes it easy to use `ParseFile`'s - as models in MVVM and SwiftUI. - - note: `id` allows `ParseFile`'s to be used even when they are not saved. - - important: `id` will have the same value as `name` when a `ParseFile` is saved. - */ - public var id: String { - guard isSaved else { - guard let cloudURL = cloudURL else { - guard let localURL = localURL else { - guard let data = data else { - return name - } - return "\(name)_\(data)" - } - return combineName(with: localURL) - } - return combineName(with: cloudURL) - } - return name - } - /** The name of the file. 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 { self.options = options } - public func hash(into hasher: inout Hasher) { - hasher.combine(self.id) - } - public func isSaved() async throws -> Bool { isSaved } @@ -174,6 +148,30 @@ public struct ParseFile: Fileable, Savable, Deletable, Hashable, Identifiable { } } +// MARK: Identifiable +extension ParseFile { + /** + A computed property that ensures `ParseFile`'s can be uniquely identified across instances. + - note: `id` allows `ParseFile`'s to be uniquely identified even if they have not been saved. + - important: `id` will have the same value as `objectId` when a `ParseObject` contains an `objectId`. + */ + public var id: String { + guard isSaved else { + guard let cloudURL = cloudURL else { + guard let localURL = localURL else { + guard let data = data else { + return name + } + return "\(name)_\(data)" + } + return combineName(with: localURL) + } + return combineName(with: cloudURL) + } + return name + } +} + // MARK: Helper Methods (internal) extension ParseFile { func combineName(with url: URL) -> String { diff --git a/Sources/ParseSwift/Types/ParseHookResponse.swift b/Sources/ParseSwift/Types/ParseHookResponse.swift index 8a9e52167..5a81377b5 100644 --- a/Sources/ParseSwift/Types/ParseHookResponse.swift +++ b/Sources/ParseSwift/Types/ParseHookResponse.swift @@ -12,7 +12,7 @@ import Foundation Build a response after processing a `ParseHookFunctionRequest` or `ParseHookTriggerRequest`. */ -public struct ParseHookResponse: ParseTypeable { +public struct ParseHookResponse: ParseTypeable { /// The data to return in the response. public var success: R? /// An object with a Parse code and message. diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index 677fc870c..336bce03f 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -132,7 +132,7 @@ public struct Query: ParseTypeable where T: ParseObject { - parameter key: The key to order by. */ - public enum Order: Codable, Equatable, Sendable { + public enum Order: ParseTypeable { /// Sort in ascending order based on `key`. case ascending(String) /// Sort in descending order based on `key`. diff --git a/Tests/ParseSwiftTests/ParseErrorTests.swift b/Tests/ParseSwiftTests/ParseErrorTests.swift index 71b5b5fe9..ce163bd6b 100644 --- a/Tests/ParseSwiftTests/ParseErrorTests.swift +++ b/Tests/ParseSwiftTests/ParseErrorTests.swift @@ -159,6 +159,23 @@ class ParseErrorTests: XCTestCase { XCTAssertNil(error.containedIn([.operationForbidden, .invalidQuery])) } + func testHashing() throws { + let error1 = ParseError(code: .accountAlreadyLinked, message: "Hello") + let error2 = ParseError(code: .accountAlreadyLinked, message: "World") + let error3 = error1 + + var setOfSameErrors = Set([error1, error1, error3]) + XCTAssertEqual(setOfSameErrors.count, 1) + XCTAssertEqual(setOfSameErrors.first, error1) + XCTAssertEqual(setOfSameErrors.first, error3) + XCTAssertNotEqual(setOfSameErrors.first, error2) + setOfSameErrors.insert(error2) + XCTAssertEqual(setOfSameErrors.count, 2) + XCTAssertTrue(setOfSameErrors.contains(error1)) + XCTAssertTrue(setOfSameErrors.contains(error2)) + XCTAssertTrue(setOfSameErrors.contains(error3)) + } + func testErrorCount() throws { let errorCodes = ParseError.Code.allCases XCTAssertGreaterThan(errorCodes.count, 50) diff --git a/Tests/ParseSwiftTests/ParseObjectAsyncTests.swift b/Tests/ParseSwiftTests/ParseObjectAsyncTests.swift index d56a2647d..5469b6c5f 100644 --- a/Tests/ParseSwiftTests/ParseObjectAsyncTests.swift +++ b/Tests/ParseSwiftTests/ParseObjectAsyncTests.swift @@ -1801,7 +1801,7 @@ class ParseObjectAsyncTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssertEqual(savedGame.objectId, gameOnServer.objectId) XCTAssertEqual(savedGame.createdAt, gameOnServer.createdAt) XCTAssertEqual(savedGame.updatedAt, gameOnServer.createdAt) - XCTAssertEqual(savedGame.profilePicture, gameOnServer.profilePicture) + XCTAssertEqual(savedGame.profilePicture?.url, gameOnServer.profilePicture?.url) } #endif }