Skip to content

Commit cee4330

Browse files
authored
feat: add HashingSource and HashingSink (#756)
1 parent e1ea10e commit cee4330

File tree

7 files changed

+182
-11
lines changed

7 files changed

+182
-11
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"id": "01b0c84d-bd45-4742-aad7-c3a1f9bd5f17",
3+
"type": "feature",
4+
"description": "Add HashingSource and HashingSink"
5+
}

runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/Canonicalizer.kt

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
package aws.smithy.kotlin.runtime.auth.awssigning
66

77
import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials
8-
import aws.smithy.kotlin.runtime.hashing.HashFunction
98
import aws.smithy.kotlin.runtime.hashing.HashSupplier
109
import aws.smithy.kotlin.runtime.hashing.Sha256
1110
import aws.smithy.kotlin.runtime.hashing.hash
@@ -17,7 +16,6 @@ import aws.smithy.kotlin.runtime.http.request.toBuilder
1716
import aws.smithy.kotlin.runtime.http.util.encodeLabel
1817
import aws.smithy.kotlin.runtime.io.*
1918
import aws.smithy.kotlin.runtime.io.internal.SdkDispatchers
20-
import aws.smithy.kotlin.runtime.io.internal.SdkSinkObserver
2119
import aws.smithy.kotlin.runtime.time.TimestampFormat
2220
import aws.smithy.kotlin.runtime.util.*
2321
import aws.smithy.kotlin.runtime.util.text.*
@@ -179,15 +177,6 @@ internal class DefaultCanonicalizer(private val sha256Supplier: HashSupplier = :
179177
}
180178
}
181179

182-
private class HashingSink(
183-
val hash: HashFunction,
184-
sink: SdkSink = SdkSink.blackhole(),
185-
) : SdkSinkObserver(sink) {
186-
override fun observe(data: ByteArray, offset: Int, length: Int) {
187-
hash.update(data, offset, length)
188-
}
189-
}
190-
191180
/** The number of bytes to read at a time during SHA256 calculation on streaming bodies. */
192181
private const val STREAM_CHUNK_BYTES = 16384 // 16KB
193182

runtime/io/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ kotlin {
2929
commonMain {
3030
dependencies {
3131
implementation(project(":runtime:utils"))
32+
implementation(project(":runtime:hashing"))
3233

3334
implementation("com.squareup.okio:okio:$okioVersion")
3435
implementation("org.jetbrains.kotlinx:atomicfu:$atomicFuVersion")
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package aws.smithy.kotlin.runtime.io
7+
8+
import aws.smithy.kotlin.runtime.hashing.HashFunction
9+
import aws.smithy.kotlin.runtime.io.internal.SdkSinkObserver
10+
import aws.smithy.kotlin.runtime.util.InternalApi
11+
12+
/**
13+
* A sink which hashes data as it is being written to
14+
* @param hash The [HashFunction] to hash data with
15+
* @param sink the [SdkSink] to hash
16+
*/
17+
@InternalApi
18+
public class HashingSink(private val hash: HashFunction, sink: SdkSink = SdkSink.blackhole()) : SdkSinkObserver(sink) {
19+
override fun observe(data: ByteArray, offset: Int, length: Int) {
20+
hash.update(data, offset, length)
21+
}
22+
23+
/**
24+
* Provides the digest as a ByteArray
25+
* @return a ByteArray representing the contents of the hash
26+
*/
27+
public fun digest(): ByteArray = hash.digest()
28+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package aws.smithy.kotlin.runtime.io
7+
8+
import aws.smithy.kotlin.runtime.hashing.HashFunction
9+
import aws.smithy.kotlin.runtime.io.internal.SdkSourceObserver
10+
import aws.smithy.kotlin.runtime.util.InternalApi
11+
12+
/**
13+
* A source which hashes data as it is being consumed
14+
* @param hash The [HashFunction] to hash data with
15+
* @param source the [SdkSource] to hash
16+
*/
17+
@InternalApi
18+
public class HashingSource(private val hash: HashFunction, source: SdkSource) : SdkSourceObserver(source) {
19+
override fun observe(data: ByteArray, offset: Int, length: Int) {
20+
hash.update(data, offset, length)
21+
}
22+
23+
/**
24+
* Provides the digest as a ByteArray
25+
* @return a ByteArray representing the contents of the hash
26+
*/
27+
public fun digest(): ByteArray = hash.digest()
28+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package aws.smithy.kotlin.runtime.io
7+
8+
import aws.smithy.kotlin.runtime.hashing.*
9+
import org.junit.jupiter.params.ParameterizedTest
10+
import org.junit.jupiter.params.provider.ValueSource
11+
import kotlin.test.assertEquals
12+
13+
class HashingSinkTest {
14+
private fun getHashFunction(name: String): HashFunction = when (name) {
15+
"crc32" -> Crc32()
16+
"crc32c" -> Crc32c()
17+
"md5" -> Md5()
18+
"sha1" -> Sha1()
19+
"sha256" -> Sha256()
20+
else -> throw RuntimeException("HashFunction $name is not supported")
21+
}
22+
23+
@ParameterizedTest
24+
@ValueSource(strings = ["crc32", "crc32c", "md5", "sha1", "sha256"])
25+
fun testHashingSinkDigest(hashFunctionName: String) = run {
26+
val byteArray = ByteArray(19456) { 0xf }
27+
val buffer = SdkBuffer()
28+
buffer.write(byteArray)
29+
30+
val hashingSink = HashingSink(getHashFunction(hashFunctionName), SdkSink.blackhole())
31+
32+
val expectedHash = getHashFunction(hashFunctionName)
33+
34+
assertEquals(expectedHash.digest().decodeToString(), hashingSink.digest().decodeToString())
35+
hashingSink.write(buffer, buffer.size)
36+
expectedHash.update(byteArray)
37+
assertEquals(expectedHash.digest().decodeToString(), hashingSink.digest().decodeToString())
38+
}
39+
40+
@ParameterizedTest
41+
@ValueSource(strings = ["crc32", "crc32c", "md5", "sha1", "sha256"])
42+
fun testHashingSinkPartialWrite(hashFunctionName: String) = run {
43+
val byteArray = ByteArray(19456) { 0xf }
44+
val buffer = SdkBuffer()
45+
buffer.write(byteArray)
46+
47+
val hashingSink = HashingSink(getHashFunction(hashFunctionName), SdkSink.blackhole())
48+
val expectedHash = getHashFunction(hashFunctionName)
49+
50+
assertEquals(expectedHash.digest().decodeToString(), hashingSink.digest().decodeToString())
51+
hashingSink.write(buffer, 512)
52+
expectedHash.update(byteArray, 0, 512)
53+
assertEquals(expectedHash.digest().decodeToString(), hashingSink.digest().decodeToString())
54+
55+
hashingSink.write(buffer, 512)
56+
expectedHash.update(byteArray, 512, 512)
57+
assertEquals(expectedHash.digest().decodeToString(), hashingSink.digest().decodeToString())
58+
}
59+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package aws.smithy.kotlin.runtime.io
7+
8+
import aws.smithy.kotlin.runtime.hashing.*
9+
import org.junit.jupiter.params.ParameterizedTest
10+
import org.junit.jupiter.params.provider.ValueSource
11+
import kotlin.test.assertEquals
12+
13+
class HashingSourceTest {
14+
private fun getHashFunction(name: String): HashFunction = when (name) {
15+
"crc32" -> Crc32()
16+
"crc32c" -> Crc32c()
17+
"md5" -> Md5()
18+
"sha1" -> Sha1()
19+
"sha256" -> Sha256()
20+
else -> throw RuntimeException("HashFunction $name is not supported")
21+
}
22+
23+
@ParameterizedTest
24+
@ValueSource(strings = ["crc32", "crc32c", "md5", "sha1", "sha256"])
25+
fun testHashingSourceDigest(hashFunctionName: String) = run {
26+
val byteArray = ByteArray(19456) { 0xf }
27+
val source = byteArray.source()
28+
val hashingSource = HashingSource(getHashFunction(hashFunctionName), source)
29+
30+
val sink = SdkBuffer()
31+
32+
val expectedHash = getHashFunction(hashFunctionName)
33+
assertEquals(expectedHash.digest().decodeToString(), hashingSource.digest().decodeToString())
34+
35+
hashingSource.read(sink, 19456)
36+
expectedHash.update(byteArray)
37+
38+
assertEquals(expectedHash.digest().decodeToString(), hashingSource.digest().decodeToString())
39+
}
40+
41+
@ParameterizedTest
42+
@ValueSource(strings = ["crc32", "crc32c", "md5", "sha1", "sha256"])
43+
fun testHashingSourcePartialRead(hashFunctionName: String) = run {
44+
val byteArray = ByteArray(19456) { 0xf }
45+
val source = byteArray.source()
46+
val hashingSource = HashingSource(getHashFunction(hashFunctionName), source)
47+
48+
val sink = SdkBuffer()
49+
50+
val expectedHash = getHashFunction(hashFunctionName)
51+
assertEquals(expectedHash.digest().decodeToString(), hashingSource.digest().decodeToString())
52+
53+
hashingSource.read(sink, 512)
54+
expectedHash.update(byteArray, 0, 512)
55+
assertEquals(expectedHash.digest().decodeToString(), hashingSource.digest().decodeToString())
56+
57+
hashingSource.read(sink, 512)
58+
expectedHash.update(byteArray, 512, 512)
59+
assertEquals(expectedHash.digest().decodeToString(), hashingSource.digest().decodeToString())
60+
}
61+
}

0 commit comments

Comments
 (0)