Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add wrapper for checksums + unit tests #642

Merged
merged 8 commits into from
Jan 12, 2024
83 changes: 83 additions & 0 deletions Sources/ClientRuntime/Networking/HashFunction.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/

import AwsCommonRuntimeKit

enum HashResult {
case data(Data)
case integer(UInt32)
}

enum HashError: Error {
case invalidInput
case hashingFailed(reason: String)
}

enum HashFunction {
case crc32, crc32c, sha1, sha256, md5

static func from(string: String) -> HashFunction? {
switch string.lowercased() {
case "crc32": return .crc32
case "crc32c": return .crc32c
case "sha1": return .sha1
case "sha256": return .sha256
case "md5": return .md5 // md5 is not a valid flexible checksum algorithm
default: return nil
}
}

var isSupported: Bool {
switch self {
case .crc32, .crc32c, .sha256, .sha1:
return true
default:
return false
}
}

func computeHash(of data: Data) throws -> HashResult {
switch self {
case .crc32:
return .integer(data.computeCRC32())
case .crc32c:
return .integer(data.computeCRC32C())
case .sha1:
do {
let hashed = try data.computeSHA1()
return .data(hashed)
} catch {
throw HashError.hashingFailed(reason: "Error computing SHA1: \(error)")
}
case .sha256:
do {
let hashed = try data.computeSHA256()
return .data(hashed)
} catch {
throw HashError.hashingFailed(reason: "Error computing SHA256: \(error)")
}
case .md5:
do {
let hashed = try data.computeMD5()
return .data(hashed)
} catch {
throw HashError.hashingFailed(reason: "Error computing MD5: \(error)")
}
}
}
}

extension HashResult {

// Convert a HashResult to a hexadecimal String
func toHexString() -> String {
switch self {
case .data(let data):
return data.map { String(format: "%02x", $0) }.joined()
case .integer(let integer):
return String(format: "%08x", integer)
}
}
}
163 changes: 163 additions & 0 deletions Tests/ClientRuntimeTests/NetworkingTests/HashFunctionTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import XCTest
@testable import ClientRuntime
import AwsCommonRuntimeKit

class HashFunctionTests: XCTestCase {

override func setUp() {
// Initialize function needs to be called before interacting with CRT
CommonRuntimeKit.initialize()
}

func testCRC32NonUTF8Bytes() {
guard let hashFunction = HashFunction.from(string: "crc32") else {
XCTFail("CRC32 not found")
return
}
Comment on lines +20 to +23
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debatable: Feel free to ignore.
I find it better and more concise to use let hashFunction = HashFunction.from(string: "crc32")! instead of a guard in tests. The test will fail anyway if we don't find it. Same comment for all the guards, .?, and try? in these tests.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removing try? causes error unhandled errors, the guard statements I prefer to keep in for precise error messaging should it ever fail

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, by the way, you can replace try? with try! and .? with .! to avoid causing any errors.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally agreed, I am less of a stickler about force-unwrap in tests, though it can be annoying for the test suite to crash when it could have just failed a test & moved on.

If the ! is expected to fail under any circumstance, I'd prefer to throw & fail versus crash the test suite.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to know! For now I am going to continue with the guard so that it is abundantly clear what the issue is if we get a failure here


// Create test data
let testBytes: [UInt8] = [0xFF, 0xFE, 0xFD, 0xFC] // Bytes not valid in UTF-8
let testData = Data(testBytes)

let computedHash = try? hashFunction.computeHash(of: testData)
guard case let .integer(result)? = computedHash else {
XCTFail("CRC32 computed hash is not an integer or is nil")
return
}
let expected = UInt32(1426237168)
XCTAssertEqual(result, expected, "CRC32 hash does not match expected value")
}

func testCRC32CNonUTF8Bytes() {
guard let hashFunction = HashFunction.from(string: "crc32c") else {
XCTFail("CRC32C not found")
return
}

// Create test data
let testBytes: [UInt8] = [0xFF, 0xFE, 0xFD, 0xFC] // Bytes not valid in UTF-8
let testData = Data(testBytes)

let computedHash = try? hashFunction.computeHash(of: testData)
guard case let .integer(result)? = computedHash else {
XCTFail("CRC32C computed hash is not an integer or is nil")
return
}
let expected = UInt32(1856745115)
XCTAssertEqual(result, expected, "CRC32C hash does not match expected value")
}

func testSHA1NonUTF8Bytes() {
guard let hashFunction = HashFunction.from(string: "sha1") else {
XCTFail("SHA1 not found")
return
}

// Create test data
let testBytes: [UInt8] = [0xFF, 0xFE, 0xFD, 0xFC] // Bytes not valid in UTF-8
let testData = Data(testBytes)

let computedHash = try? hashFunction.computeHash(of: testData)
guard case let .data(result)? = computedHash else {
XCTFail("SHA1 computed hash is not a data type or is nil")
return
}
let expected = "ADfJtWg8Do2MpnFNsvFRmyMuEOI="
XCTAssertEqual(result.base64EncodedString(), expected, "SHA1 hash does not match expected value")
}

func testSHA256NonUTF8Bytes() {
guard let hashFunction = HashFunction.from(string: "sha256") else {
XCTFail("SHA256 not found")
return
}

// Create test data
let testBytes: [UInt8] = [0xFF, 0xFE, 0xFD, 0xFC] // Bytes not valid in UTF-8
let testData = Data(testBytes)

let computedHash = try? hashFunction.computeHash(of: testData)
guard case let .data(result)? = computedHash else {
XCTFail("SHA256 computed hash is not a data type or is nil")
return
}
let expected = "jCosV0rEcc6HWQwT8O/bQr0ssZuxhJM3nUW/zJBgtlc="
XCTAssertEqual(result.base64EncodedString(), expected, "SHA256 hash does not match expected value")
}

func testMD5NonUTF8Bytes() {
guard let hashFunction = HashFunction.from(string: "md5") else {
XCTFail("MD5 not found")
return
}

// Create test data
let testBytes: [UInt8] = [0xFF, 0xFE, 0xFD, 0xFC] // Bytes not valid in UTF-8
let testData = Data(testBytes)

let computedHash = try? hashFunction.computeHash(of: testData)
guard case let .data(result)? = computedHash else {
XCTFail("MD5 computed hash is not a data type or is nil")
return
}
let expected = "ilWq/WLcPzYHQ8fAzwCCLg=="
XCTAssertEqual(result.base64EncodedString(), expected, "MD5 hash does not match expected value")
}

func testInvalidHashFunction() {
let invalidHashFunction = HashFunction.from(string: "invalid")
XCTAssertNil(invalidHashFunction, "Invalid hash function should return nil")
}

func testHashFunctionToHexString() {
let testData = Data("Hello, world!".utf8)

// CRC32
if let crc32Function = HashFunction.from(string: "crc32"),
let crc32Result = try? crc32Function.computeHash(of: testData).toHexString() {
XCTAssertEqual(crc32Result, "ebe6c6e6", "CRC32 hexadecimal representation does not match expected value")
} else {
XCTFail("CRC32 hash function not found or computation failed")
}

// CRC32C
if let crc32cFunction = HashFunction.from(string: "crc32c"),
let crc32cResult = try? crc32cFunction.computeHash(of: testData).toHexString() {
XCTAssertEqual(crc32cResult, "c8a106e5", "CRC32C hexadecimal representation does not match expected value")
} else {
XCTFail("CRC32C hash function not found or computation failed")
}

// SHA1
if let sha1Function = HashFunction.from(string: "sha1"),
let sha1Result = try? sha1Function.computeHash(of: testData).toHexString() {
XCTAssertEqual(sha1Result, "943a702d06f34599aee1f8da8ef9f7296031d699", "SHA1 hexadecimal representation does not match expected value")
} else {
XCTFail("SHA1 hash function not found or computation failed")
}

// SHA256
if let sha256Function = HashFunction.from(string: "sha256"),
let sha256Result = try? sha256Function.computeHash(of: testData).toHexString() {
XCTAssertEqual(sha256Result, "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3", "SHA256 hexadecimal representation does not match expected value")
} else {
XCTFail("SHA256 hash function not found or computation failed")
}

// MD5
if let md5Function = HashFunction.from(string: "md5"),
let md5Result = try? md5Function.computeHash(of: testData).toHexString() {
XCTAssertEqual(md5Result, "6cd3556deb0da54bca060b4c39479839", "MD5 hexadecimal representation does not match expected value")
} else {
XCTFail("MD5 hash function not found or computation failed")
}
}

}
Loading