Skip to content

Commit e202c3e

Browse files
authored
Fix Counter.Bit64.Final.asBits calculation (#12)
1 parent 38c3e3d commit e202c3e

File tree

4 files changed

+123
-19
lines changed

4 files changed

+123
-19
lines changed

library/bits/build.gradle.kts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,13 @@ plugins {
1818
}
1919

2020
kmpConfiguration {
21-
configureShared(java9ModuleName = "org.kotlincrypto.bitops.bits", publish = true) {}
21+
configureShared(java9ModuleName = "org.kotlincrypto.bitops.bits", publish = true) {
22+
jvm {
23+
sourceSetTest {
24+
dependencies {
25+
implementation(project(":library:endian"))
26+
}
27+
}
28+
}
29+
}
2230
}

library/bits/src/commonMain/kotlin/org/kotlincrypto/bitops/bits/Counter.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ public sealed class Counter private constructor() {
278278

279279
public override fun asBits(): Final {
280280
if (isBits) return this
281-
return Final(lo shl 3, (hi shl 3) or (lo ushr 29), isBits = true)
281+
return Final(lo shl 3, (hi shl 3) or (lo ushr 61), isBits = true)
282282
}
283283
}
284284

library/bits/src/commonTest/kotlin/org/kotlincrypto/bitops/bits/CounterUnitTest.kt

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -209,21 +209,4 @@ class CounterUnitTest {
209209
val actual = ((bits.hi.toLong() and 0xffffffff) shl 32) or (bits.lo.toLong() and 0xffffffff)
210210
assertEquals(expected, actual)
211211
}
212-
213-
@Test
214-
fun givenBit64Final_whenAsBits_thenConvertsAsExpected() {
215-
val number = 5551889119L
216-
val final = Counter.Bit64.Final(lo = number, hi = 1)
217-
val bits = final.asBits()
218-
assertNotEquals(final, bits)
219-
220-
// Should return same instance (already bits)
221-
assertEquals(bits, bits.asBits())
222-
223-
var expected = Long.MAX_VALUE
224-
expected += number + 1
225-
expected *= Byte.SIZE_BITS
226-
val actual = ((bits.hi and 0xffffffff) shl 32) or (bits.lo and 0xffffffff)
227-
assertEquals(expected, actual)
228-
}
229212
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* Copyright (c) 2025 Matthew Nelson
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
**/
16+
package org.kotlincrypto.bitops.bits
17+
18+
import org.kotlincrypto.bitops.endian.Endian.Big.beLongAt
19+
import java.math.BigInteger
20+
import kotlin.random.Random
21+
import kotlin.test.Test
22+
import kotlin.test.assertEquals
23+
24+
/**
25+
* Uses Java's [BigInteger] to verify [Counter.Bit64.Final.asBits] functions as expected.
26+
* */
27+
class CounterJvmUnitTest {
28+
29+
@Test
30+
fun givenBit64Final_whenAsMaxBits_thenConvertsAsExpected() {
31+
val max = BigInteger("2").pow(128)
32+
val maxBytes = max.divide(BigInteger("8"))
33+
val maxBytesMinus1 = maxBytes.minus(BigInteger.ONE)
34+
val expectedBits = maxBytesMinus1.times(BigInteger("8"))
35+
36+
val bMaxBytesMinus1 = maxBytesMinus1.toByteArray()
37+
val bExpectedBits = expectedBits.toByteArray()
38+
39+
val bits = Counter.Bit64.Final(
40+
lo = bMaxBytesMinus1.beLongAt(8),
41+
hi = bMaxBytesMinus1.beLongAt(0),
42+
).asBits()
43+
44+
// BigInteger.toByteArray returns 17 bytes, prefixed with
45+
// a leading 0 which is dropped.
46+
val expectedLo = bExpectedBits.beLongAt(9)
47+
val expectedHi = bExpectedBits.beLongAt(1)
48+
49+
assertEquals(-8L, expectedLo)
50+
assertEquals(-1L, expectedHi)
51+
52+
assertEquals(expectedLo, bits.lo)
53+
assertEquals(expectedHi, bits.hi)
54+
}
55+
56+
@Test
57+
fun givenBit64Final_whenAsBits_thenConvertsAsExpected() {
58+
val pow67 = BigInteger("2").pow(67).minus(BigInteger.ONE)
59+
val expectedBits = pow67.times(BigInteger("8"))
60+
61+
// BigInteger.toByteArray returns 9 bytes (most significant
62+
// number stripped of leading zeros)
63+
val bPow67 = pow67.toByteArray()
64+
val bExpectedBits = expectedBits.toByteArray()
65+
66+
val bits = Counter.Bit64.Final(
67+
lo = bPow67.beLongAt(1),
68+
hi = bPow67[0].toLong(),
69+
).asBits()
70+
71+
val expectedLo = bExpectedBits.beLongAt(1)
72+
val expectedHi = bExpectedBits[0].toLong()
73+
74+
assertEquals(-8, expectedLo)
75+
assertEquals(63, expectedHi)
76+
77+
assertEquals(expectedLo, bits.lo)
78+
assertEquals(expectedHi, bits.hi)
79+
}
80+
81+
@Test
82+
fun givenBit64Final_whenAsBitsFromRandomLargeNumber_thenConvertsAsExpected() {
83+
repeat(500) {
84+
val number = BigInteger("2").pow(88).minus(
85+
BigInteger(
86+
Random.Default.nextLong(Long.MAX_VALUE - 500_000_000, Long.MAX_VALUE).toString()
87+
)
88+
)
89+
val nBits = number.times(BigInteger("8"))
90+
91+
val bNumber = number.toByteArray().to16Bytes()
92+
val bNBits = nBits.toByteArray().to16Bytes()
93+
94+
val bits = Counter.Bit64.Final(
95+
lo = bNumber.beLongAt(8),
96+
hi = bNumber.beLongAt(0),
97+
).asBits()
98+
99+
val expectedLo = bNBits.beLongAt(8)
100+
val expectedHi = bNBits.beLongAt(0)
101+
102+
assertEquals(expectedLo, bits.lo)
103+
assertEquals(expectedHi, bits.hi)
104+
}
105+
}
106+
107+
private fun ByteArray.to16Bytes(): ByteArray {
108+
require(this.size <= 16) { "array.size is greater than 16 bytes..." }
109+
val b = ByteArray(16)
110+
copyInto(b, destinationOffset = 16 - size)
111+
return b
112+
}
113+
}

0 commit comments

Comments
 (0)