Skip to content

Commit 37d02ef

Browse files
author
Chad Parry
committed
Fix #124: Manage entropy bits in TimeBasedEpochGenerator
1 parent 314366d commit 37d02ef

File tree

2 files changed

+120
-25
lines changed

2 files changed

+120
-25
lines changed

src/main/java/com/fasterxml/uuid/impl/TimeBasedEpochGenerator.java

+36-25
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
import java.security.SecureRandom;
44
import java.util.Random;
55
import java.util.UUID;
6-
import java.util.concurrent.locks.Lock;
7-
import java.util.concurrent.locks.ReentrantLock;
6+
import java.util.function.Consumer;
87

98
import com.fasterxml.uuid.NoArgGenerator;
109
import com.fasterxml.uuid.UUIDClock;
@@ -35,9 +34,9 @@ public class TimeBasedEpochGenerator extends NoArgGenerator
3534
*/
3635

3736
/**
38-
* Random number generator that this generator uses.
37+
* Random number generator that fills a byte array with entropy
3938
*/
40-
protected final Random _random;
39+
protected final Consumer<byte[]> _randomNextBytes;
4140

4241
/**
4342
* Underlying {@link UUIDClock} used for accessing current time, to use for
@@ -49,7 +48,6 @@ public class TimeBasedEpochGenerator extends NoArgGenerator
4948

5049
private long _lastTimestamp = -1;
5150
private final byte[] _lastEntropy = new byte[ENTROPY_BYTE_LENGTH];
52-
private final Lock lock = new ReentrantLock();
5351

5452
/*
5553
/**********************************************************************
@@ -76,10 +74,12 @@ public TimeBasedEpochGenerator(Random rnd) {
7674
*/
7775
public TimeBasedEpochGenerator(Random rnd, UUIDClock clock)
7876
{
79-
if (rnd == null) {
80-
rnd = LazyRandom.sharedSecureRandom();
81-
}
82-
_random = rnd;
77+
this((rnd == null ? LazyRandom.sharedSecureRandom() : rnd)::nextBytes, clock);
78+
}
79+
80+
TimeBasedEpochGenerator(Consumer<byte[]> randomNextBytes, UUIDClock clock)
81+
{
82+
_randomNextBytes = randomNextBytes;
8383
_clock = clock;
8484
}
8585

@@ -120,28 +120,39 @@ public UUID generate()
120120
*/
121121
public UUID construct(long rawTimestamp)
122122
{
123-
lock.lock();
124-
try {
123+
final long mostSigBits, leastSigBits;
124+
synchronized (_lastEntropy) {
125125
if (rawTimestamp == _lastTimestamp) {
126-
boolean c = true;
127-
for (int i = ENTROPY_BYTE_LENGTH - 1; i >= 0; i--) {
128-
if (c) {
129-
byte temp = _lastEntropy[i];
130-
temp = (byte) (temp + 0x01);
131-
c = _lastEntropy[i] == (byte) 0xff;
132-
_lastEntropy[i] = temp;
126+
carry:
127+
{
128+
for (int i = ENTROPY_BYTE_LENGTH - 1; i > 0; i--) {
129+
_lastEntropy[i] = (byte) (_lastEntropy[i] + 1);
130+
if (_lastEntropy[i] != 0x00) {
131+
break carry;
132+
}
133+
}
134+
_lastEntropy[0] = (byte) (_lastEntropy[0] + 1);
135+
if (_lastEntropy[0] >= 0x04) {
136+
throw new IllegalStateException("overflow on same millisecond");
133137
}
134-
}
135-
if (c) {
136-
throw new IllegalStateException("overflow on same millisecond");
137138
}
138139
} else {
139140
_lastTimestamp = rawTimestamp;
140-
_random.nextBytes(_lastEntropy);
141+
_randomNextBytes.accept(_lastEntropy);
142+
// In the most significant byte, only 2 bits will fit in the UUID, and one of those should be cleared
143+
// to guard against overflow.
144+
_lastEntropy[0] &= 0x01;
141145
}
142-
return UUIDUtil.constructUUID(UUIDType.TIME_BASED_EPOCH, (rawTimestamp << 16) | _toShort(_lastEntropy, 0), _toLong(_lastEntropy, 2));
143-
} finally {
144-
lock.unlock();
146+
mostSigBits = rawTimestamp << 16 |
147+
(long) UUIDType.TIME_BASED_EPOCH.raw() << 12 |
148+
Byte.toUnsignedLong(_lastEntropy[0]) << 10 |
149+
Byte.toUnsignedLong(_lastEntropy[1]) << 2 |
150+
Byte.toUnsignedLong(_lastEntropy[2]) >>> 6;
151+
long right62Mask = (1L << 62) - 1;
152+
long variant = 0x02;
153+
leastSigBits = variant << 62 |
154+
_toLong(_lastEntropy, 2) & right62Mask;
145155
}
156+
return new UUID(mostSigBits, leastSigBits);
146157
}
147158
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package com.fasterxml.uuid.impl;
2+
3+
import com.fasterxml.uuid.UUIDClock;
4+
import junit.framework.TestCase;
5+
6+
import java.math.BigInteger;
7+
import java.util.Arrays;
8+
import java.util.UUID;
9+
import java.util.function.Consumer;
10+
11+
public class TimeBasedEpochGeneratorTest extends TestCase {
12+
13+
public void testFormat() {
14+
BigInteger minEntropy = BigInteger.ZERO;
15+
long minTimestamp = 0;
16+
TimeBasedEpochGenerator generatorEmpty = new TimeBasedEpochGenerator(staticEntropy(minEntropy), staticClock(minTimestamp));
17+
UUID uuidEmpty = generatorEmpty.generate();
18+
assertEquals(0x07, uuidEmpty.version());
19+
assertEquals(0x02, uuidEmpty.variant());
20+
assertEquals(minTimestamp, getTimestamp(uuidEmpty));
21+
assertEquals(minEntropy, getEntropy(uuidEmpty));
22+
23+
Consumer<byte[]> entropyFull = bytes -> Arrays.fill(bytes, (byte) 0xFF);
24+
long maxTimestamp = rightBitmask(48);
25+
TimeBasedEpochGenerator generatorFull = new TimeBasedEpochGenerator(entropyFull, staticClock(maxTimestamp));
26+
UUID uuidFull = generatorFull.generate();
27+
assertEquals(0x07, uuidFull.version());
28+
assertEquals(0x02, uuidFull.variant());
29+
assertEquals(maxTimestamp, getTimestamp(uuidFull));
30+
assertEquals(BigInteger.ONE.shiftLeft(73).subtract(BigInteger.ONE), getEntropy(uuidFull));
31+
}
32+
33+
public void testIncrement() {
34+
TimeBasedEpochGenerator generator = new TimeBasedEpochGenerator(staticEntropy(BigInteger.ZERO), staticClock(0));
35+
assertEquals(BigInteger.valueOf(0), getEntropy(generator.generate()));
36+
assertEquals(BigInteger.valueOf(1), getEntropy(generator.generate()));
37+
assertEquals(BigInteger.valueOf(2), getEntropy(generator.generate()));
38+
assertEquals(BigInteger.valueOf(3), getEntropy(generator.generate()));
39+
}
40+
41+
public void testCarryOnce() {
42+
TimeBasedEpochGenerator generator = new TimeBasedEpochGenerator(staticEntropy(BigInteger.valueOf(0xFF)), staticClock(0));
43+
assertEquals(BigInteger.valueOf(0xFF), getEntropy(generator.generate()));
44+
assertEquals(BigInteger.valueOf(0x100), getEntropy(generator.generate()));
45+
}
46+
47+
public void testCarryAll() {
48+
BigInteger largeEntropy = BigInteger.ONE.shiftLeft(73).subtract(BigInteger.ONE);
49+
TimeBasedEpochGenerator generator = new TimeBasedEpochGenerator(staticEntropy(largeEntropy), staticClock(0));
50+
assertEquals(largeEntropy, getEntropy(generator.generate()));
51+
assertEquals(BigInteger.ONE.shiftLeft(73), getEntropy(generator.generate()));
52+
}
53+
54+
private long getTimestamp(UUID uuid) {
55+
return uuid.getMostSignificantBits() >>> 16;
56+
}
57+
58+
private BigInteger getEntropy(UUID uuid) {
59+
return BigInteger.valueOf(uuid.getMostSignificantBits() & rightBitmask(12)).shiftLeft(62).or(
60+
BigInteger.valueOf(uuid.getLeastSignificantBits() & rightBitmask(62)));
61+
}
62+
63+
private Consumer<byte[]> staticEntropy(BigInteger entropy) {
64+
byte[] entropyBytes = entropy.toByteArray();
65+
return bytes -> {
66+
int offset = bytes.length - entropyBytes.length;
67+
Arrays.fill(bytes, 0, offset, (byte) 0x00);
68+
System.arraycopy(entropyBytes, 0, bytes, offset, entropyBytes.length);
69+
};
70+
}
71+
72+
private UUIDClock staticClock(long timestamp) {
73+
return new UUIDClock() {
74+
@Override
75+
public long currentTimeMillis() {
76+
return timestamp;
77+
}
78+
};
79+
}
80+
81+
private long rightBitmask(int bits) {
82+
return (1L << bits) - 1;
83+
}
84+
}

0 commit comments

Comments
 (0)