Skip to content

Commit

Permalink
fix: Improve ParseObject conformance to Hashable (#176)
Browse files Browse the repository at this point in the history
* fix: Improve ParseObject conformance to Hashable

* nit

* another nit

* Test ParseError hashing

* Use compiler level equatable for ParseFile

* fix file test

* improve identifiable documentation
  • Loading branch information
cbaker6 authored Jul 13, 2024
1 parent b3e84a2 commit 173e7d8
Show file tree
Hide file tree
Showing 13 changed files with 76 additions and 64 deletions.
10 changes: 8 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
25 changes: 10 additions & 15 deletions Sources/ParseSwift/Objects/ParseObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
7 changes: 0 additions & 7 deletions Sources/ParseSwift/Objects/ParseRole.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion Sources/ParseSwift/ParseConstants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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/"
Expand Down
8 changes: 0 additions & 8 deletions Sources/ParseSwift/Protocols/Fileable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
2 changes: 1 addition & 1 deletion Sources/ParseSwift/Protocols/ParseHookParametable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
2 changes: 1 addition & 1 deletion Sources/ParseSwift/Protocols/ParseTypeable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import Foundation
*/
public protocol ParseTypeable: Codable,
Sendable,
Equatable,
Hashable,
CustomDebugStringConvertible,
CustomStringConvertible {}

Expand Down
11 changes: 11 additions & 0 deletions Sources/ParseSwift/Types/ParseError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
50 changes: 24 additions & 26 deletions Sources/ParseSwift/Types/ParseFile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion Sources/ParseSwift/Types/ParseHookResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import Foundation
Build a response after processing a `ParseHookFunctionRequest`
or `ParseHookTriggerRequest`.
*/
public struct ParseHookResponse<R: Codable & Equatable & Sendable>: ParseTypeable {
public struct ParseHookResponse<R: Codable & Hashable & Sendable>: ParseTypeable {
/// The data to return in the response.
public var success: R?
/// An object with a Parse code and message.
Expand Down
2 changes: 1 addition & 1 deletion Sources/ParseSwift/Types/Query.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ public struct Query<T>: 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`.
Expand Down
17 changes: 17 additions & 0 deletions Tests/ParseSwiftTests/ParseErrorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion Tests/ParseSwiftTests/ParseObjectAsyncTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

0 comments on commit 173e7d8

Please sign in to comment.