-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Improve chunk upload in Swift
(box/box-codegen#515)
(#143)
- Loading branch information
1 parent
9e0b4e2
commit b8099ab
Showing
3 changed files
with
218 additions
and
85 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
{ "engineHash": "06b32a5", "specHash": "ee83bc7", "version": "0.1.0" } | ||
{ "engineHash": "62c749a", "specHash": "ee83bc7", "version": "0.1.0" } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,107 +1,237 @@ | ||
import Foundation | ||
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) | ||
import CommonCrypto | ||
#endif | ||
|
||
/// `SHA1` class provides an interface for calculating SHA-1 hash using different implementations based on platform availability. | ||
internal class SHA1: SHA1Calculator { | ||
private let sha1Calculator: SHA1Calculator | ||
|
||
/// Initializes `SHA1` instance and selects appropriate SHA-1 calculator based on platform | ||
init() { | ||
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) | ||
sha1Calculator = SHA1CommonCrypto() | ||
#else | ||
sha1Calculator = SHA1Raw() | ||
#endif | ||
} | ||
|
||
/// Updates the SHA-1 calculation with the given data chunk. | ||
/// | ||
/// - Parameter data: The data chunk to update the hash calculation. | ||
func update(data: Data) { | ||
sha1Calculator.update(data: data) | ||
} | ||
|
||
/// Finalizes the SHA-1 calculation and returns the computed hash. | ||
/// | ||
/// - Returns: The computed SHA-1 hash as `Data`. | ||
func finalize() -> Data { | ||
return sha1Calculator.finalize() | ||
} | ||
} | ||
|
||
/// Protocol defining methods required for SHA-1 calculation. | ||
private protocol SHA1Calculator { | ||
/// Updates the hash calculation with the given data. | ||
/// | ||
/// - Parameter data: The data to update the hash with. | ||
func update(data: Data) | ||
|
||
/// Finalizes the hash calculation and returns the computed hash. | ||
/// | ||
/// - Returns: The computed hash as `Data`. | ||
func finalize() -> Data | ||
} | ||
|
||
struct SHA1 { | ||
// Hash initial values | ||
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) | ||
/// `SHA1CommonCrypto` class provides SHA-1 calculation using CommonCrypto library. | ||
private class SHA1CommonCrypto: SHA1Calculator { | ||
private var context = CC_SHA1_CTX() | ||
|
||
init() { | ||
CC_SHA1_Init(&context) | ||
} | ||
|
||
/// Updates the SHA-1 calculation with the given data chunk using CommonCrypto. | ||
/// | ||
/// - Parameter data: The data chunk to update the hash calculation. | ||
func update(data: Data) { | ||
data.withUnsafeBytes { (bufferPointer: UnsafeRawBufferPointer) in | ||
guard let unsafeBufferPointerBaseAddress = bufferPointer.baseAddress else { return } | ||
let unsafePointer = unsafeBufferPointerBaseAddress.assumingMemoryBound(to: UInt8.self) | ||
CC_SHA1_Update(&context, unsafePointer, CC_LONG(data.count)) | ||
} | ||
} | ||
|
||
/// Finalizes the SHA-1 calculation using CommonCrypto and returns the computed hash. | ||
/// | ||
/// - Returns: The computed SHA-1 hash as `Data`. | ||
func finalize() -> Data { | ||
var digest = [UInt8](repeating: 0, count: Int(CC_SHA1_DIGEST_LENGTH)) | ||
CC_SHA1_Final(&digest, &context) | ||
return Data(digest) | ||
} | ||
} | ||
#endif | ||
|
||
/// `SHA1Raw` class provides SHA-1 calculation using raw implementation. | ||
private class SHA1Raw: SHA1Calculator { | ||
private static let h0: UInt32 = 0x67452301 | ||
private static let h1: UInt32 = 0xEFCDAB89 | ||
private static let h2: UInt32 = 0x98BADCFE | ||
private static let h3: UInt32 = 0x10325476 | ||
private static let h4: UInt32 = 0xC3D2E1F0 | ||
|
||
static func sha1(data: Data) -> Data { | ||
var message = data | ||
let messageLength = UInt64(message.count * 8) | ||
private static let k1: UInt32 = 0x5A827999 | ||
private static let k2: UInt32 = 0x6ED9EBA1 | ||
private static let k3: UInt32 = 0x8F1BBCDC | ||
private static let k4: UInt32 = 0xCA62C1D6 | ||
|
||
private var currentHash: [UInt32] = [SHA1Raw.h0, SHA1Raw.h1, SHA1Raw.h2, SHA1Raw.h3, SHA1Raw.h4] | ||
private var messageLength: UInt64 = 0 | ||
private var buffer: [UInt8] = [] | ||
|
||
private var w = [UInt32](repeating: 0, count: 80) | ||
|
||
/// Updates the SHA-1 calculation with the given data chunk using raw implementation. | ||
/// | ||
/// - Parameter data: The data chunk to update the hash calculation. | ||
func update(data: Data) { | ||
data.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) in | ||
guard let baseAddress = bytes.baseAddress?.assumingMemoryBound(to: UInt8.self) else { return } | ||
let byteCount = bytes.count | ||
|
||
messageLength &+= UInt64(byteCount * 8) | ||
var dataPointer = baseAddress | ||
|
||
if !buffer.isEmpty { | ||
let remainingSpace = 64 - buffer.count | ||
if byteCount >= remainingSpace { | ||
buffer.append(contentsOf: UnsafeBufferPointer(start: dataPointer, count: remainingSpace)) | ||
buffer.withUnsafeBytes { bufferPointer in | ||
processBlock(bufferPointer.baseAddress!.assumingMemoryBound(to: UInt8.self)) | ||
} | ||
buffer.removeAll(keepingCapacity: true) | ||
dataPointer = dataPointer.advanced(by: remainingSpace) | ||
} else { | ||
buffer.append(contentsOf: UnsafeBufferPointer(start: dataPointer, count: byteCount)) | ||
return | ||
} | ||
} | ||
|
||
// Padding the message | ||
message.append(0x80) | ||
while (message.count % 64) != 56 { | ||
message.append(0x00) | ||
while dataPointer.distance(to: baseAddress.advanced(by: byteCount)) >= 64 { | ||
processBlock(dataPointer) | ||
dataPointer = dataPointer.advanced(by: 64) | ||
} | ||
|
||
buffer.append(contentsOf: UnsafeBufferPointer(start: dataPointer, count: dataPointer.distance(to: baseAddress.advanced(by: byteCount)))) | ||
} | ||
} | ||
|
||
/// Finalizes the SHA-1 calculation and returns the computed hash. | ||
/// | ||
/// - Returns: The computed SHA-1 hash as `Data`. | ||
func finalize() -> Data { | ||
var paddingLength = 64 - ((buffer.count + 8) % 64) | ||
if paddingLength == 0 { paddingLength = 64 } | ||
|
||
buffer.append(0x80) | ||
buffer.append(contentsOf: [UInt8](repeating: 0, count: paddingLength - 1)) | ||
|
||
// Append the length of the original message as a 64-bit big-endian integer | ||
let lengthBytes = messageLength.bigEndian | ||
message.append(value: lengthBytes) | ||
let lengthBytes = withUnsafeBytes(of: messageLength.bigEndian) { Array($0) } | ||
buffer.append(contentsOf: lengthBytes) | ||
|
||
// Initialize hash values | ||
var h:[UInt32]=[SHA1.h0,SHA1.h1,SHA1.h2,SHA1.h3,SHA1.h4] | ||
var hashBuffer = [UInt8]() | ||
hashBuffer.reserveCapacity(20) | ||
|
||
// Process the message in successive 512-bit chunks | ||
for chunkOffset in stride(from: 0, to: message.count, by: 64) { | ||
let chunk = message[chunkOffset..<chunkOffset + 64] | ||
var words = [UInt32](repeating: 0, count: 80) | ||
buffer.withUnsafeBytes { bufferPointer in | ||
let bufferBaseAddress = bufferPointer.baseAddress!.assumingMemoryBound(to: UInt8.self) | ||
let bufferCount = buffer.count | ||
var dataPointer = bufferBaseAddress | ||
|
||
// Break chunk into sixteen 32-bit big-endian words | ||
for i in 0..<16 { | ||
let start = chunk.index(chunk.startIndex, offsetBy: i * 4) | ||
let end = chunk.index(start, offsetBy: 4) | ||
words[i] = UInt32(bigEndian: chunk[start..<end].withUnsafeBytes { $0.load(as: UInt32.self) }) | ||
while dataPointer.distance(to: bufferBaseAddress.advanced(by: bufferCount)) >= 64 { | ||
processBlock(dataPointer) | ||
dataPointer = dataPointer.advanced(by: 64) | ||
} | ||
|
||
// Extend the sixteen 32-bit words into eighty 32-bit words | ||
for i in 16..<80 { | ||
words[i] = leftRotate(words[i - 3] ^ words[i - 8] ^ words[i - 14] ^ words[i - 16], by: 1) | ||
// Convert UInt32 values in currentHash to big-endian bytes and append to hashBuffer | ||
for value in currentHash { | ||
let valueBytes = [ | ||
UInt8((value >> 24) & 0xFF), | ||
UInt8((value >> 16) & 0xFF), | ||
UInt8((value >> 8) & 0xFF), | ||
UInt8(value & 0xFF) | ||
] | ||
hashBuffer.append(contentsOf: valueBytes) | ||
} | ||
} | ||
|
||
// Initialize hash value for this chunk | ||
var a = h[0] | ||
var b = h[1] | ||
var c = h[2] | ||
var d = h[3] | ||
var e = h[4] | ||
|
||
// Main loop | ||
for i in 0..<80 { | ||
var f: UInt32 = 0 | ||
var k: UInt32 = 0 | ||
|
||
switch i { | ||
case 0...19: | ||
f = (b & c) | ((bitwiseNot(b)) & d) | ||
k = 0x5A827999 | ||
case 20...39: | ||
f = b ^ c ^ d | ||
k = 0x6ED9EBA1 | ||
case 40...59: | ||
f = (b & c) | (b & d) | (c & d) | ||
k = 0x8F1BBCDC | ||
case 60...79: | ||
f = b ^ c ^ d | ||
k = 0xCA62C1D6 | ||
default: | ||
break | ||
} | ||
return Data(hashBuffer) | ||
} | ||
|
||
let temp = leftRotate(a, by: 5) &+ f &+ e &+ k &+ words[i] | ||
e = d | ||
d = c | ||
c = leftRotate(b, by: 30) | ||
b = a | ||
a = temp | ||
} | ||
/// Processes a block of data during SHA-1 calculation. | ||
/// | ||
/// - Parameter chunk: The chunk of data to process. | ||
private func processBlock(_ chunk: UnsafePointer<UInt8>) { | ||
for i in 0..<16 { | ||
w[i] = (UInt32(chunk[4 * i]) << 24) | (UInt32(chunk[4 * i + 1]) << 16) | | ||
(UInt32(chunk[4 * i + 2]) << 8) | (UInt32(chunk[4 * i + 3])) | ||
} | ||
|
||
// Add this chunk's hash to result so far | ||
h[0] = h[0] &+ a | ||
h[1] = h[1] &+ b | ||
h[2] = h[2] &+ c | ||
h[3] = h[3] &+ d | ||
h[4] = h[4] &+ e | ||
for i in 16..<80 { | ||
w[i] = SHA1Raw.rotateLeft(w[i-3] ^ w[i-8] ^ w[i-14] ^ w[i-16], bits: 1) | ||
} | ||
|
||
// Produce the final hash value (big-endian) | ||
var hash = Data() | ||
[h[0], h[1], h[2], h[3], h[4]].forEach { | ||
let bigEndianValue = $0.bigEndian | ||
hash.append(value: bigEndianValue) | ||
var a = currentHash[0] | ||
var b = currentHash[1] | ||
var c = currentHash[2] | ||
var d = currentHash[3] | ||
var e = currentHash[4] | ||
|
||
for i in 0..<80 { | ||
let f: UInt32 | ||
let k: UInt32 | ||
|
||
switch i { | ||
case 0..<20: | ||
f = (b & c) | (SHA1Raw.bitwiseNot(b) & d) | ||
k = SHA1Raw.k1 | ||
case 20..<40: | ||
f = b ^ c ^ d | ||
k = SHA1Raw.k2 | ||
case 40..<60: | ||
f = (b & c) | (b & d) | (c & d) | ||
k = SHA1Raw.k3 | ||
case 60..<80: | ||
f = b ^ c ^ d | ||
k = SHA1Raw.k4 | ||
default: | ||
fatalError("Should not reach here") | ||
} | ||
|
||
let temp = SHA1Raw.rotateLeft(a, bits: 5) &+ f &+ e &+ k &+ w[i] | ||
e = d | ||
d = c | ||
c = SHA1Raw.rotateLeft(b, bits: 30) | ||
b = a | ||
a = temp | ||
} | ||
|
||
return hash | ||
currentHash[0] &+= a | ||
currentHash[1] &+= b | ||
currentHash[2] &+= c | ||
currentHash[3] &+= d | ||
currentHash[4] &+= e | ||
} | ||
|
||
private static func bitwiseNot<T: FixedWidthInteger>(_ value: T) -> T { | ||
return value ^ T.max | ||
@inline(__always) | ||
private static func rotateLeft(_ value: UInt32, bits: UInt32) -> UInt32 { | ||
return (value << bits) | (value >> (32 - bits)) | ||
} | ||
|
||
private static func leftRotate(_ value: UInt32, by bits: UInt32) -> UInt32 { | ||
return (value << bits) | (value >> (32 - bits)) | ||
@inline(__always) | ||
private static func bitwiseNot<T: FixedWidthInteger>(_ value: T) -> T { | ||
return value ^ T.max | ||
} | ||
} |