Skip to content

Commit a79c167

Browse files
authored
feat: add support for native checksums (#100)
1 parent 4b10e82 commit a79c167

File tree

10 files changed

+220
-1004
lines changed

10 files changed

+220
-1004
lines changed

.github/workflows/ci.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ jobs:
3333
- name: Test with ${{ matrix.java-version }}
3434
shell: bash
3535
run: |
36+
./gradlew apiCheck
3637
./gradlew -Ptest.java.version=${{ matrix.java-version }} -Paws.sdk.kotlin.crt.disableCrossCompile=true jvmTest --stacktrace
3738
- name: Save Test Reports
3839
if: failure()

aws-crt-kotlin/api/aws-crt-kotlin.api

Lines changed: 0 additions & 996 deletions
This file was deleted.

aws-crt-kotlin/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ kotlin {
177177
val xcrun = "/usr/bin/xcrun"
178178

179179
tasks.register<Exec>("bootIosSimulatorDevice") {
180+
isIgnoreExitValue = true
180181
commandLine(xcrun, "simctl", "boot", simulatorDeviceName)
181182

182183
doLast {

aws-crt-kotlin/common/test/aws/sdk/kotlin/crt/util/DigestTest.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@
55

66
package aws.sdk.kotlin.crt.util
77

8-
import kotlin.test.Ignore
8+
import aws.sdk.kotlin.crt.CrtTest
99
import kotlin.test.Test
1010
import kotlin.test.assertEquals
1111

12-
class DigestTest {
13-
@Ignore // FIXME Enable when Kotlin/Native implementation is complete
12+
class DigestTest : CrtTest() {
1413
@Test
1514
fun testSha256() {
1615
val buffer = "I don't know half of you half as well as I should like; and I like less than half of you half as well as you deserve.".encodeToByteArray()

aws-crt-kotlin/native/interop/crt.def

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ headers = aws/common/allocator.h \
1919
aws/http/connection_manager.h \
2020
aws/http/request_response.h \
2121
aws/http/proxy.h \
22-
aws/compression/compression.h
23-
headerFilter = aws/common/* aws/io/* aws/http/* aws/compression/*
22+
aws/compression/compression.h \
23+
aws/cal/hash.h \
24+
aws/checksums/crc.h
25+
headerFilter = aws/common/* aws/io/* aws/http/* aws/compression/* aws/cal/* aws/checksums/*
2426

2527
linkerOpts.osx = -framework Security
2628
linkerOpts.ios = -framework Security

aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/util/DigestNative.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@
55

66
package aws.sdk.kotlin.crt.util
77

8+
import Sha256
9+
810
/**
911
* Utility object for various hash functions
1012
*/
1113
public actual object Digest {
1214
/**
1315
* Calculate the SHA-256 hash of the input [buffer]
1416
*/
15-
public actual fun sha256(buffer: ByteArray): ByteArray {
16-
TODO("Not yet implemented")
17-
}
17+
public actual fun sha256(buffer: ByteArray): ByteArray = Sha256().apply {
18+
update(buffer, 0, buffer.size)
19+
}.digest()
1820
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import kotlinx.cinterop.*
2+
import libcrt.aws_checksums_crc32
3+
import libcrt.aws_checksums_crc32c
4+
import platform.posix.uint32_t
5+
import platform.posix.uint8_tVar
6+
7+
/**
8+
* A typealias to CRT's aws_checksums_<crc32/crc32c> functions
9+
*/
10+
internal typealias AwsChecksumsCrcFunction = (
11+
input: CValuesRef<uint8_tVar>?,
12+
length: Int,
13+
previousCrc32: uint32_t
14+
) -> uint32_t
15+
16+
internal class Crc(val checksumFn: AwsChecksumsCrcFunction) : HashFunction {
17+
private var crc = 0U
18+
19+
override fun update(input: ByteArray, offset: Int, length: Int) {
20+
val offsetInput = input.usePinned {
21+
it.addressOf(offset)
22+
}
23+
crc = checksumFn(offsetInput.reinterpret(), length.convert(), crc)
24+
}
25+
26+
override fun digest(): ByteArray = byteArrayOf(
27+
((crc shr 24) and 0xffu).toByte(),
28+
((crc shr 16) and 0xffu).toByte(),
29+
((crc shr 8) and 0xffu).toByte(),
30+
(crc and 0xffu).toByte(),
31+
).also { reset() }
32+
33+
override fun reset() {
34+
crc = 0U
35+
}
36+
}
37+
38+
/**
39+
* A CRC32 [HashFunction] implemented using bindings to CRT.
40+
*/
41+
public class Crc32 : HashFunction {
42+
private val crc32 = Crc(::aws_checksums_crc32)
43+
override fun update(input: ByteArray, offset: Int, length: Int) { crc32.update(input, offset, length) }
44+
override fun digest(): ByteArray = crc32.digest()
45+
override fun reset() { crc32.reset() }
46+
}
47+
48+
/**
49+
* A CRC32C [HashFunction] implemented using bindings to CRT.
50+
*/
51+
public class Crc32c : HashFunction {
52+
private val crc32c = Crc(::aws_checksums_crc32c)
53+
override fun update(input: ByteArray, offset: Int, length: Int) { crc32c.update(input, offset, length) }
54+
override fun digest(): ByteArray = crc32c.digest()
55+
override fun reset() { crc32c.reset() }
56+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* A function which calculates the hash of given data
3+
*/
4+
public interface HashFunction {
5+
/**
6+
* Update the hash content with [length] bytes from [input] starting from [offset]
7+
*/
8+
public fun update(input: ByteArray, offset: Int, length: Int)
9+
10+
/**
11+
* Digest the hash content, returning [ByteArray] and resetting the hash
12+
*/
13+
public fun digest(): ByteArray
14+
15+
/**
16+
* Reset the content of the hash
17+
*/
18+
public fun reset()
19+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
import aws.sdk.kotlin.crt.Allocator
6+
import aws.sdk.kotlin.crt.CrtRuntimeException
7+
import aws.sdk.kotlin.crt.awsAssertOpSuccess
8+
import kotlinx.cinterop.*
9+
import libcrt.*
10+
11+
/**
12+
* A typealias to CRT's aws_<checksum_algorithm>_new functions (i.e. aws_sha1_new, aws_sha256_new))
13+
*/
14+
internal typealias InitializeHashFn = (
15+
allocator: CValuesRef<aws_allocator>?,
16+
) -> CPointer<aws_hash>?
17+
18+
/**
19+
* SHA-1 hash function implemented using bindings to CRT
20+
*/
21+
public class Sha1 : HashFunction {
22+
private val sha1 = Sha(::aws_sha1_new)
23+
override fun update(input: ByteArray, offset: Int, length: Int) { sha1.update(input, offset, length) }
24+
override fun digest(): ByteArray = sha1.digest()
25+
override fun reset() { sha1.reset() }
26+
}
27+
28+
/**
29+
* SHA-256 hash function implemented using bindings to CRT
30+
*/
31+
public class Sha256 : HashFunction {
32+
private val sha256 = Sha(::aws_sha256_new)
33+
override fun update(input: ByteArray, offset: Int, length: Int) { sha256.update(input, offset, length) }
34+
override fun digest(): ByteArray = sha256.digest()
35+
override fun reset() { sha256.reset() }
36+
}
37+
38+
internal class Sha(val initializeFn: InitializeHashFn) : HashFunction {
39+
private var hash: CPointer<aws_hash>
40+
41+
init {
42+
hash = initializeHash()
43+
}
44+
45+
// aws_hash_update
46+
override fun update(input: ByteArray, offset: Int, length: Int) {
47+
val inputCursor = input.usePinned {
48+
aws_byte_cursor_from_array(it.addressOf(offset), length.convert())
49+
}
50+
awsAssertOpSuccess(aws_hash_update(hash, inputCursor)) {
51+
"aws_hash_update"
52+
}
53+
}
54+
55+
// aws_hash_finalize
56+
override fun digest(): ByteArray {
57+
val output = ByteArray(hash.pointed.digest_size.toInt())
58+
59+
val byteBuf = output.usePinned {
60+
cValue<aws_byte_buf> {
61+
capacity = output.size.convert()
62+
len = 0U
63+
buffer = it.addressOf(0).reinterpret()
64+
}
65+
}
66+
67+
awsAssertOpSuccess(aws_hash_finalize(hash, byteBuf, 0U)) { "aws_hash_finalize" }
68+
69+
return output.also { reset() }
70+
}
71+
72+
// aws_hash_destroy
73+
override fun reset() {
74+
aws_hash_destroy(hash)
75+
hash = initializeHash()
76+
}
77+
78+
private fun initializeHash() = checkNotNull(initializeFn(Allocator.Default.allocator)) {
79+
"failed to initialize hash"
80+
}
81+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
import aws.sdk.kotlin.crt.util.encodeToHex
6+
import kotlin.test.Test
7+
import kotlin.test.assertEquals
8+
9+
class HashFunctionTest {
10+
@Test
11+
fun testUpdate() {
12+
// algorithm -> listOf(hash("uh"), hash(""))
13+
val tests = listOf(
14+
(Sha1() to listOf("80c3d7b3f509a5ac8bb29d7f8c4a94af14f7d244", "da39a3ee5e6b4b0d3255bfef95601890afd80709")),
15+
(Sha256() to listOf("beba745afae8503925089cc2f3cc9b87e849e81c07531e83c5c341a63bcaaed9", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")),
16+
(Crc32() to listOf("50f87626", "00000000")),
17+
(Crc32c() to listOf("f80f9153", "00000000")),
18+
)
19+
20+
tests.forEach { (hash, expected) ->
21+
val data = "uh".encodeToByteArray()
22+
hash.update(data, 0, data.size)
23+
24+
// hash of "uh"
25+
assertEquals(expected[0], hash.digest().encodeToHex())
26+
27+
// hash of an empty string
28+
assertEquals(expected[1], hash.digest().encodeToHex())
29+
}
30+
}
31+
32+
@Test
33+
fun testMultipleUpdatesWithOffset() {
34+
// algorithm -> hash("uh" + "huh")
35+
val tests = listOf(
36+
(Sha1() to "c093098e7a1a8ec547e82a399d63f331760e9a57"),
37+
(Sha256() to "b474fb57f69c1b5aa00aa27ad0b36c03fe9915cc497344d6a55a86b2e9bd1b72"),
38+
(Crc32() to "5e67b90f"),
39+
(Crc32c() to "a3249200"),
40+
)
41+
42+
tests.forEach { (hash, expected) ->
43+
val data = "ahuhi".encodeToByteArray()
44+
hash.update(data, 2, 2) // uh
45+
46+
hash.update(data, 1, 3) // huh
47+
48+
assertEquals(expected, hash.digest().encodeToHex())
49+
}
50+
}
51+
}

0 commit comments

Comments
 (0)