Skip to content

Commit

Permalink
Add namespaced UUIDs v3 and v5 (benasher44#87)
Browse files Browse the repository at this point in the history
  • Loading branch information
benasher44 authored Jul 12, 2020
1 parent e67f02d commit cdf8d40
Show file tree
Hide file tree
Showing 13 changed files with 20,573 additions and 52 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
project adheres to [Semantic Versioning](https://semver.org/).

## [0.1.1] - 2020-07-12
### Added
- Add namespaced UUIDs v3 and v5 (#87)

## [0.1.0] - 2020-03-03
### Added
- Comparable support for `Uuid` (#72)
Expand Down
68 changes: 54 additions & 14 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinNativeLink
import org.jetbrains.kotlin.konan.target.HostManager

plugins {
Expand Down Expand Up @@ -37,7 +36,7 @@ kotlin {
iosArm32()
}
if (HostManager.hostIsMingw || HostManager.hostIsMac) {
mingwX64() {
mingwX64 {
binaries.findTest(DEBUG)!!.linkerOpts = mutableListOf("-Wl,--subsystem,windows")
}
}
Expand All @@ -59,13 +58,13 @@ kotlin {
}
}

val nix64MainSourceSets = listOf(
val nix64MainSourceDirs = listOf(
"src/nonJvmMain/kotlin",
"src/nativeMain/kotlin",
"src/nix64Main/kotlin"
)

val nix32MainSourceSets = listOf(
val nix32MainSourceDirs = listOf(
"src/nonJvmMain/kotlin",
"src/nativeMain/kotlin",
"src/nix32Main/kotlin"
Expand Down Expand Up @@ -94,14 +93,22 @@ kotlin {
}
}

val macosX64Main by getting { kotlin.srcDirs(nix64MainSourceSets) }
val macosX64Test by getting { kotlin.srcDir("src/cocoaTest/kotlin") }
val iosArm64Main by getting { kotlin.srcDirs(nix64MainSourceSets) }
val iosArm64Test by getting { kotlin.srcDir("src/cocoaTest/kotlin") }
val iosArm32Main by getting { kotlin.srcDirs(nix32MainSourceSets) }
val iosArm32Test by getting { kotlin.srcDir("src/cocoaTest/kotlin") }
val iosX64Main by getting { kotlin.srcDirs(nix64MainSourceSets) }
val iosX64Test by getting { kotlin.srcDir("src/cocoaTest/kotlin") }
val appleMain32SourceDirs = listOf(
"src/appleMain/kotlin"
) + nix32MainSourceDirs

val appleMain64SourceDirs = listOf(
"src/appleMain/kotlin"
) + nix64MainSourceDirs

val macosX64Main by getting { kotlin.srcDirs(appleMain64SourceDirs) }
val macosX64Test by getting { kotlin.srcDir("src/appleTest/kotlin") }
val iosArm64Main by getting { kotlin.srcDirs(appleMain64SourceDirs) }
val iosArm64Test by getting { kotlin.srcDir("src/appleTest/kotlin") }
val iosArm32Main by getting { kotlin.srcDirs(appleMain32SourceDirs) }
val iosArm32Test by getting { kotlin.srcDir("src/appleTest/kotlin") }
val iosX64Main by getting {kotlin.srcDirs(appleMain64SourceDirs) }
val iosX64Test by getting { kotlin.srcDir("src/appleTest/kotlin") }
}
if (HostManager.hostIsMingw || HostManager.hostIsMac) {
val mingwX64Main by getting {
Expand All @@ -113,10 +120,13 @@ kotlin {
)
)
}
val mingwX64Test by getting {
kotlin.srcDir("src/mingwTest/kotlin")
}
}
if (HostManager.hostIsLinux || HostManager.hostIsMac) {
val linuxX64Main by getting { kotlin.srcDirs(nix64MainSourceSets) }
val linuxArm32HfpMain by getting { kotlin.srcDirs(nix32MainSourceSets) }
val linuxX64Main by getting { kotlin.srcDirs(nix64MainSourceDirs) }
val linuxArm32HfpMain by getting { kotlin.srcDirs(nix32MainSourceDirs) }
}
}
}
Expand Down Expand Up @@ -158,3 +168,33 @@ checkTask.configure {
}

apply(from = "publish.gradle")

/// Generate PROJECT_DIR_ROOT for referencing local mocks in tests

val projectDirGenRoot = "$buildDir/generated/projectdir/kotlin"
val generateProjDirValTask = tasks.register("generateProjectDirectoryVal") {
doLast {
mkdir(projectDirGenRoot)
val projDirFile = File("$projectDirGenRoot/projdir.kt")
projDirFile.writeText("")
projDirFile.appendText("""
|package com.benasher44.uuid
|
|import kotlin.native.concurrent.SharedImmutable
|
|@SharedImmutable
|internal const val PROJECT_DIR_ROOT = ""${'"'}${projectDir.absolutePath}""${'"'}
|
""".trimMargin())
}
}

kotlin.sourceSets.named("commonTest") {
this.kotlin.srcDir(projectDirGenRoot)
}
// Ensure this runs before any test compile task
tasks.withType<AbstractCompile>().configureEach {
if (name.toLowerCase().contains("test")) {
dependsOn(generateProjDirValTask)
}
}
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ kotlin.code.style=official
kotlin.incremental=true

GROUP=com.benasher44
VERSION=0.1.1-SNAPSHOT
VERSION=0.1.1

POM_URL=https://github.com/benasher44/uuid/
POM_SCM_URL=https://github.com/benasher44/uuid/
Expand Down
78 changes: 78 additions & 0 deletions src/appleMain/kotlin/namebased.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.benasher44.uuid

import kotlinx.cinterop.addressOf
import kotlinx.cinterop.reinterpret
import kotlinx.cinterop.usePinned
import platform.CoreCrypto.CC_MD5
import platform.CoreCrypto.CC_MD5_DIGEST_LENGTH
import platform.CoreCrypto.CC_SHA1
import platform.CoreCrypto.CC_SHA1_DIGEST_LENGTH

/**
* Constructs a "Name-Based" version 3 [UUID][Uuid].
*
* Version 3 UUIDs are created by combining a name and
* a namespace using the MD5 hash function.
*
* @param namespace for the "Name-Based" UUID
* @param name withing the namespace for the "Name-Based" UUID
* @return New version 3 [UUID][Uuid].
* @see <a href="https://tools.ietf.org/html/rfc4122#section-4.3">RFC 4122: Section 4.3</a>
*/
@ExperimentalStdlibApi
public fun uuid3Of(namespace: Uuid, name: String): Uuid =
nameBasedUuidOf(namespace, name, AppleHasher(AppleHasher.Companion::md5Digest, 3))

/**
* Constructs a "Name-Based" version 5 [UUID][Uuid].
*
* Version 5 UUIDs are created by combining a name and
* a namespace using the SHA-1 hash function.
*
* @param namespace for the "Name-Based" UUID
* @param name withing the namespace for the "Name-Based" UUID
* @return New version 5 [UUID][Uuid].
* @see <a href="https://tools.ietf.org/html/rfc4122#section-4.3">RFC 4122: Section 4.3</a>
*/
@ExperimentalStdlibApi
public fun uuid5Of(namespace: Uuid, name: String): Uuid =
nameBasedUuidOf(namespace, name, AppleHasher(AppleHasher.Companion::sha1Digest, 5))

private class AppleHasher(
private val digestFunc: (ByteArray) -> ByteArray,
override val version: Int
) : UuidHasher {
private var data = ByteArray(0)

override fun update(input: ByteArray) {
val prevLength = data.size
data = data.copyOf(data.size + input.size)
input.copyInto(data, prevLength)
}

override fun digest(): ByteArray {
return digestFunc(data)
}

companion object {
fun sha1Digest(data: ByteArray): ByteArray {
return ByteArray(CC_SHA1_DIGEST_LENGTH).also { bytes ->
bytes.usePinned { digestPin ->
data.usePinned { dataPin ->
CC_SHA1(dataPin.addressOf(0), data.size.toUInt(), digestPin.addressOf(0).reinterpret())
}
}
}
}

fun md5Digest(data: ByteArray): ByteArray {
return ByteArray(CC_MD5_DIGEST_LENGTH).also { bytes ->
bytes.usePinned { digestPin ->
data.usePinned { dataPin ->
CC_MD5(dataPin.addressOf(0), data.size.toUInt(), digestPin.addressOf(0).reinterpret())
}
}
}
}
}
}
80 changes: 80 additions & 0 deletions src/appleTest/kotlin/AppleUuidTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.benasher44.uuid

import kotlin.native.concurrent.isFrozen
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlinx.cinterop.ByteVar
import kotlinx.cinterop.addressOf
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.readBytes
import kotlinx.cinterop.reinterpret
import kotlinx.cinterop.usePinned
import platform.Foundation.NSData
import platform.Foundation.NSUUID
import platform.Foundation.dataWithContentsOfFile

@ExperimentalStdlibApi
class CocoaUuidTest {
@Test
fun `UUID.toString() matches NSUUID`() {
val uuidL = uuid4()
val nativeUuidString = uuidL.bytes.usePinned {
NSUUID(it.addressOf(0).reinterpret()).UUIDString
}.toLowerCase()
assertEquals(uuidL.toString(), nativeUuidString)
}

@Test
fun `UUID bytes match NSUUID`() {
val uuidL = uuid4()
val nativeUuid = NSUUID(uuidL.toString())
val nativeBytes = ByteArray(UUID_BYTES)
nativeBytes.usePinned {
nativeUuid.getUUIDBytes(it.addressOf(0).reinterpret())
}
assertTrue(uuidL.bytes.contentEquals(nativeBytes))
}

@Test
fun `UUID is frozen after initialization`() {
assertTrue(uuid4().isFrozen)
}

@Test
fun `test uuid5`() {
enumerateUuid5Data { namespace, name, result ->
assertEquals(result, uuid5Of(namespace, name))
}
}

@Test
fun `test uuid3`() {
enumerateUuid3Data { namespace, name, result ->
assertEquals(result, uuid3Of(namespace, name))
}
}
}

private fun enumerateUuid3Data(enumerationLambda: (namespace: Uuid, name: String, result: Uuid) -> Unit) {
enumerateData("src/commonTest/data/uuid3.txt", enumerationLambda)
}

private fun enumerateUuid5Data(enumerationLambda: (namespace: Uuid, name: String, result: Uuid) -> Unit) {
enumerateData("src/commonTest/data/uuid5.txt", enumerationLambda)
}

private fun enumerateData(path: String, enumerationLambda: (namespace: Uuid, name: String, result: Uuid) -> Unit) {
val data = NSData.dataWithContentsOfFile("$PROJECT_DIR_ROOT/$path")!!
val str = memScoped {
data.bytes!!.getPointer(this)
.reinterpret<ByteVar>()
.readBytes(data.length.toInt())
.decodeToString()
}
for (row in str.split("\n")) {
if (row.isEmpty()) continue
val (namespaceStr, name, resultStr) = row.split(",")
enumerationLambda(uuidFrom(namespaceStr), name, uuidFrom(resultStr))
}
}
37 changes: 0 additions & 37 deletions src/cocoaTest/kotlin/CocoaUuidTest.kt

This file was deleted.

60 changes: 60 additions & 0 deletions src/commonMain/kotlin/uuid.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

package com.benasher44.uuid

import kotlin.experimental.and
import kotlin.experimental.or
import kotlin.native.concurrent.SharedImmutable

// Number of bytes in a UUID
Expand Down Expand Up @@ -122,3 +124,61 @@ public expect fun uuidOf(bytes: ByteArray): Uuid
*/
// @SinceKotlin("1.x")
public expect fun uuid4(): Uuid

/**
* Interface for computing a hash for a "Name-Based" UUID
*/
public interface UuidHasher {

/**
* The UUID version, for which this
* hash algorithm is being used:
* - 3 for MD5
* - 5 for SHA-1
*/
public val version: Int

/**
* Updates the hash's digest with more bytes
* @param input to update the hasher's digest
*/
public fun update(input: ByteArray)

/**
* Completes the hash computation and returns the result
* @note The hasher should not be used after this call
*/
public fun digest(): ByteArray
}

/**
* Constructs a "Name-Based" version 3 or 5 [UUID][Uuid].
*
* Version 3 and 5 UUIDs are created by combining a name and
* a namespace using a hash function. This library may provide
* such hash functions in the future, but it adds a significant
* maintenance burden to support for native, JS, and JVM. Until then:
*
* - Provide a MD5 [UuidHasher] to get a v3 UUID
* - Provide a SHA-1 [UuidHasher] to get a v5 UUID
*
* @param namespace for the "Name-Based" UUID
* @param name withing the namespace for the "Name-Based" UUID
* @param hasher interface that implements a hashing algorithm
* @return New version 3 or 5 [UUID][Uuid].
* @sample com.benasher44.uuid.uuid5Of
* @see <a href="https://tools.ietf.org/html/rfc4122#section-4.3">RFC 4122: Section 4.3</a>
*/
@ExperimentalStdlibApi
public fun nameBasedUuidOf(namespace: Uuid, name: String, hasher: UuidHasher): Uuid {
hasher.update(namespace.bytes)
hasher.update(name.encodeToByteArray())
val hashedBytes = hasher.digest()
hashedBytes[6] = hashedBytes[6]
.and(0b00001111) // clear the 4 most sig bits
.or(hasher.version.shl(4).toByte())
hashedBytes[8] = hashedBytes[8]
.and(0b00111111) // clear the 2 most sig bits
.or(-0b10000000) // set 2 most sig to 10
return uuidOf(hashedBytes.copyOf(UUID_BYTES))
}
Loading

0 comments on commit cdf8d40

Please sign in to comment.