Skip to content

Commit 1bc24de

Browse files
authored
Fix #124: Manage entropy bits in TimeBasedEpochGenerator (#125)
1 parent 85eacf0 commit 1bc24de

File tree

5 files changed

+142
-26
lines changed

5 files changed

+142
-26
lines changed

release-notes/CREDITS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,7 @@ Daniel Albuquerque (worldtiki@github)
151151
Alexander Ilinykh (divinenickname@github)
152152
* Contributed improvements to README.md, pom.xml (OSGi inclusion)
153153
[5.1.1]
154+
155+
Chad Parry (chadparry@github)
156+
* Contributed #124: TimeBasedEpochGenerator should prevent overflow
157+
[5.3.0]

release-notes/VERSION

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ Releases
66

77
5.2.0 (not yet released)
88

9+
#124: TimeBasedEpochGenerator should prevent overflow
10+
(Chad P)
911
- Update to `oss-parent` v69
1012

1113
5.1.1 (26-Sep-2025)

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

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package com.fasterxml.uuid.impl;
22

33
import java.security.SecureRandom;
4+
import java.util.Objects;
45
import java.util.Random;
56
import java.util.UUID;
6-
import java.util.concurrent.locks.Lock;
7-
import java.util.concurrent.locks.ReentrantLock;
7+
import java.util.function.Consumer;
88

99
import com.fasterxml.uuid.NoArgGenerator;
1010
import com.fasterxml.uuid.UUIDClock;
@@ -35,9 +35,11 @@ public class TimeBasedEpochGenerator extends NoArgGenerator
3535
*/
3636

3737
/**
38-
* Random number generator that this generator uses.
38+
* Source for random numbers used to fill a byte array with entropy.
39+
*
40+
* @since 5.3 (replaced earlier {@code java.util.Random _random})
3941
*/
40-
protected final Random _random;
42+
protected final Consumer<byte[]> _randomNextBytes;
4143

4244
/**
4345
* Underlying {@link UUIDClock} used for accessing current time, to use for
@@ -49,7 +51,6 @@ public class TimeBasedEpochGenerator extends NoArgGenerator
4951

5052
private long _lastTimestamp = -1;
5153
private final byte[] _lastEntropy = new byte[ENTROPY_BYTE_LENGTH];
52-
private final Lock lock = new ReentrantLock();
5354

5455
/*
5556
/**********************************************************************
@@ -76,10 +77,21 @@ public TimeBasedEpochGenerator(Random rnd) {
7677
*/
7778
public TimeBasedEpochGenerator(Random rnd, UUIDClock clock)
7879
{
79-
if (rnd == null) {
80-
rnd = LazyRandom.sharedSecureRandom();
81-
}
82-
_random = rnd;
80+
this((rnd == null ? LazyRandom.sharedSecureRandom() : rnd)::nextBytes, clock);
81+
}
82+
83+
/**
84+
*
85+
* @param randomNextBytes Source for random numbers to use for generating UUIDs.
86+
* Note that it is strongly recommend to use a <b>good</b> (pseudo) random number source;
87+
* for example, JDK's {@code SecureRandom::nextBytes}.
88+
* @param clock clock Object used for accessing current time to use for generation
89+
*
90+
* @since 5.3
91+
*/
92+
protected TimeBasedEpochGenerator(Consumer<byte[]> randomNextBytes, UUIDClock clock)
93+
{
94+
_randomNextBytes = Objects.requireNonNull(randomNextBytes);
8395
_clock = clock;
8496
}
8597

@@ -120,28 +132,39 @@ public UUID generate()
120132
*/
121133
public UUID construct(long rawTimestamp)
122134
{
123-
lock.lock();
124-
try {
135+
final long mostSigBits, leastSigBits;
136+
synchronized (_lastEntropy) {
125137
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;
138+
carry:
139+
{
140+
for (int i = ENTROPY_BYTE_LENGTH - 1; i > 0; i--) {
141+
_lastEntropy[i] = (byte) (_lastEntropy[i] + 1);
142+
if (_lastEntropy[i] != 0x00) {
143+
break carry;
144+
}
145+
}
146+
_lastEntropy[0] = (byte) (_lastEntropy[0] + 1);
147+
if (_lastEntropy[0] >= 0x04) {
148+
throw new IllegalStateException("overflow on same millisecond");
133149
}
134-
}
135-
if (c) {
136-
throw new IllegalStateException("overflow on same millisecond");
137150
}
138151
} else {
139152
_lastTimestamp = rawTimestamp;
140-
_random.nextBytes(_lastEntropy);
153+
_randomNextBytes.accept(_lastEntropy);
154+
// In the most significant byte, only 2 bits will fit in the UUID, and one of those should be cleared
155+
// to guard against overflow.
156+
_lastEntropy[0] &= 0x01;
141157
}
142-
return UUIDUtil.constructUUID(UUIDType.TIME_BASED_EPOCH, (rawTimestamp << 16) | _toShort(_lastEntropy, 0), _toLong(_lastEntropy, 2));
143-
} finally {
144-
lock.unlock();
158+
mostSigBits = rawTimestamp << 16 |
159+
(long) UUIDType.TIME_BASED_EPOCH.raw() << 12 |
160+
Byte.toUnsignedLong(_lastEntropy[0]) << 10 |
161+
Byte.toUnsignedLong(_lastEntropy[1]) << 2 |
162+
Byte.toUnsignedLong(_lastEntropy[2]) >>> 6;
163+
long right62Mask = (1L << 62) - 1;
164+
long variant = 0x02;
165+
leastSigBits = variant << 62 |
166+
_toLong(_lastEntropy, 2) & right62Mask;
145167
}
168+
return new UUID(mostSigBits, leastSigBits);
146169
}
147170
}

src/test/java/com/fasterxml/uuid/UUIDComparatorTest.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222

2323
import com.fasterxml.uuid.impl.TimeBasedEpochGenerator;
2424

25-
import com.fasterxml.uuid.impl.TimeBasedEpochRandomGenerator;
2625
import junit.framework.TestCase;
2726

2827
public class UUIDComparatorTest
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package com.fasterxml.uuid.impl;
2+
3+
import java.math.BigInteger;
4+
import java.util.Arrays;
5+
import java.util.UUID;
6+
import java.util.function.Consumer;
7+
8+
import com.fasterxml.uuid.UUIDClock;
9+
10+
import junit.framework.TestCase;
11+
12+
/**
13+
* @since 5.3
14+
*/
15+
public class TimeBasedEpochGeneratorTest extends TestCase
16+
{
17+
public void testFormat() {
18+
BigInteger minEntropy = BigInteger.ZERO;
19+
long minTimestamp = 0;
20+
TimeBasedEpochGenerator generatorEmpty = new TimeBasedEpochGenerator(staticEntropy(minEntropy), staticClock(minTimestamp));
21+
UUID uuidEmpty = generatorEmpty.generate();
22+
assertEquals(0x07, uuidEmpty.version());
23+
assertEquals(0x02, uuidEmpty.variant());
24+
assertEquals(minTimestamp, getTimestamp(uuidEmpty));
25+
assertEquals(minEntropy, getEntropy(uuidEmpty));
26+
27+
Consumer<byte[]> entropyFull = bytes -> Arrays.fill(bytes, (byte) 0xFF);
28+
long maxTimestamp = rightBitmask(48);
29+
TimeBasedEpochGenerator generatorFull = new TimeBasedEpochGenerator(entropyFull, staticClock(maxTimestamp));
30+
UUID uuidFull = generatorFull.generate();
31+
assertEquals(0x07, uuidFull.version());
32+
assertEquals(0x02, uuidFull.variant());
33+
assertEquals(maxTimestamp, getTimestamp(uuidFull));
34+
assertEquals(BigInteger.ONE.shiftLeft(73).subtract(BigInteger.ONE), getEntropy(uuidFull));
35+
}
36+
37+
public void testIncrement() {
38+
TimeBasedEpochGenerator generator = new TimeBasedEpochGenerator(staticEntropy(BigInteger.ZERO), staticClock(0));
39+
assertEquals(BigInteger.valueOf(0), getEntropy(generator.generate()));
40+
assertEquals(BigInteger.valueOf(1), getEntropy(generator.generate()));
41+
assertEquals(BigInteger.valueOf(2), getEntropy(generator.generate()));
42+
assertEquals(BigInteger.valueOf(3), getEntropy(generator.generate()));
43+
}
44+
45+
public void testCarryOnce() {
46+
TimeBasedEpochGenerator generator = new TimeBasedEpochGenerator(staticEntropy(BigInteger.valueOf(0xFF)), staticClock(0));
47+
assertEquals(BigInteger.valueOf(0xFF), getEntropy(generator.generate()));
48+
assertEquals(BigInteger.valueOf(0x100), getEntropy(generator.generate()));
49+
}
50+
51+
public void testCarryAll() {
52+
BigInteger largeEntropy = BigInteger.ONE.shiftLeft(73).subtract(BigInteger.ONE);
53+
TimeBasedEpochGenerator generator = new TimeBasedEpochGenerator(staticEntropy(largeEntropy), staticClock(0));
54+
assertEquals(largeEntropy, getEntropy(generator.generate()));
55+
assertEquals(BigInteger.ONE.shiftLeft(73), getEntropy(generator.generate()));
56+
}
57+
58+
private long getTimestamp(UUID uuid) {
59+
return uuid.getMostSignificantBits() >>> 16;
60+
}
61+
62+
private BigInteger getEntropy(UUID uuid) {
63+
return BigInteger.valueOf(uuid.getMostSignificantBits() & rightBitmask(12)).shiftLeft(62).or(
64+
BigInteger.valueOf(uuid.getLeastSignificantBits() & rightBitmask(62)));
65+
}
66+
67+
private Consumer<byte[]> staticEntropy(BigInteger entropy) {
68+
byte[] entropyBytes = entropy.toByteArray();
69+
return bytes -> {
70+
int offset = bytes.length - entropyBytes.length;
71+
Arrays.fill(bytes, 0, offset, (byte) 0x00);
72+
System.arraycopy(entropyBytes, 0, bytes, offset, entropyBytes.length);
73+
};
74+
}
75+
76+
private UUIDClock staticClock(long timestamp) {
77+
return new UUIDClock() {
78+
@Override
79+
public long currentTimeMillis() {
80+
return timestamp;
81+
}
82+
};
83+
}
84+
85+
private long rightBitmask(int bits) {
86+
return (1L << bits) - 1;
87+
}
88+
}

0 commit comments

Comments
 (0)