Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions release-notes/CREDITS
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,7 @@ Daniel Albuquerque (worldtiki@github)
Alexander Ilinykh (divinenickname@github)
* Contributed improvements to README.md, pom.xml (OSGi inclusion)
[5.1.1]

Chad Parry (chadparry@github)
* Contributed #124: TimeBasedEpochGenerator should prevent overflow
[5.3.0]
2 changes: 2 additions & 0 deletions release-notes/VERSION
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Releases

5.2.0 (not yet released)

#124: TimeBasedEpochGenerator should prevent overflow
(Chad P)
- Update to `oss-parent` v69

5.1.1 (26-Sep-2025)
Expand Down
73 changes: 48 additions & 25 deletions src/main/java/com/fasterxml/uuid/impl/TimeBasedEpochGenerator.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package com.fasterxml.uuid.impl;

import java.security.SecureRandom;
import java.util.Objects;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;

import com.fasterxml.uuid.NoArgGenerator;
import com.fasterxml.uuid.UUIDClock;
Expand Down Expand Up @@ -35,9 +35,11 @@ public class TimeBasedEpochGenerator extends NoArgGenerator
*/

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

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

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

/*
/**********************************************************************
Expand All @@ -76,10 +77,21 @@ public TimeBasedEpochGenerator(Random rnd) {
*/
public TimeBasedEpochGenerator(Random rnd, UUIDClock clock)
{
if (rnd == null) {
rnd = LazyRandom.sharedSecureRandom();
}
_random = rnd;
this((rnd == null ? LazyRandom.sharedSecureRandom() : rnd)::nextBytes, clock);
}

/**
*
* @param randomNextBytes Source for random numbers to use for generating UUIDs.
* Note that it is strongly recommend to use a <b>good</b> (pseudo) random number source;
* for example, JDK's {@code SecureRandom::nextBytes}.
* @param clock clock Object used for accessing current time to use for generation
*
* @since 5.3
*/
protected TimeBasedEpochGenerator(Consumer<byte[]> randomNextBytes, UUIDClock clock)
{
_randomNextBytes = Objects.requireNonNull(randomNextBytes);
_clock = clock;
}

Expand Down Expand Up @@ -120,28 +132,39 @@ public UUID generate()
*/
public UUID construct(long rawTimestamp)
{
lock.lock();
try {
final long mostSigBits, leastSigBits;
synchronized (_lastEntropy) {
if (rawTimestamp == _lastTimestamp) {
boolean c = true;
for (int i = ENTROPY_BYTE_LENGTH - 1; i >= 0; i--) {
if (c) {
byte temp = _lastEntropy[i];
temp = (byte) (temp + 0x01);
c = _lastEntropy[i] == (byte) 0xff;
_lastEntropy[i] = temp;
carry:
{
for (int i = ENTROPY_BYTE_LENGTH - 1; i > 0; i--) {
_lastEntropy[i] = (byte) (_lastEntropy[i] + 1);
if (_lastEntropy[i] != 0x00) {
break carry;
}
}
_lastEntropy[0] = (byte) (_lastEntropy[0] + 1);
if (_lastEntropy[0] >= 0x04) {
throw new IllegalStateException("overflow on same millisecond");
}
}
if (c) {
throw new IllegalStateException("overflow on same millisecond");
}
} else {
_lastTimestamp = rawTimestamp;
_random.nextBytes(_lastEntropy);
_randomNextBytes.accept(_lastEntropy);
// In the most significant byte, only 2 bits will fit in the UUID, and one of those should be cleared
// to guard against overflow.
_lastEntropy[0] &= 0x01;
}
return UUIDUtil.constructUUID(UUIDType.TIME_BASED_EPOCH, (rawTimestamp << 16) | _toShort(_lastEntropy, 0), _toLong(_lastEntropy, 2));
} finally {
lock.unlock();
mostSigBits = rawTimestamp << 16 |
(long) UUIDType.TIME_BASED_EPOCH.raw() << 12 |
Byte.toUnsignedLong(_lastEntropy[0]) << 10 |
Byte.toUnsignedLong(_lastEntropy[1]) << 2 |
Byte.toUnsignedLong(_lastEntropy[2]) >>> 6;
long right62Mask = (1L << 62) - 1;
long variant = 0x02;
leastSigBits = variant << 62 |
_toLong(_lastEntropy, 2) & right62Mask;
}
return new UUID(mostSigBits, leastSigBits);
}
}
1 change: 0 additions & 1 deletion src/test/java/com/fasterxml/uuid/UUIDComparatorTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@

import com.fasterxml.uuid.impl.TimeBasedEpochGenerator;

import com.fasterxml.uuid.impl.TimeBasedEpochRandomGenerator;
import junit.framework.TestCase;

public class UUIDComparatorTest
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.fasterxml.uuid.impl;

import java.math.BigInteger;
import java.util.Arrays;
import java.util.UUID;
import java.util.function.Consumer;

import com.fasterxml.uuid.UUIDClock;

import junit.framework.TestCase;

/**
* @since 5.3
*/
public class TimeBasedEpochGeneratorTest extends TestCase
{
public void testFormat() {
BigInteger minEntropy = BigInteger.ZERO;
long minTimestamp = 0;
TimeBasedEpochGenerator generatorEmpty = new TimeBasedEpochGenerator(staticEntropy(minEntropy), staticClock(minTimestamp));
UUID uuidEmpty = generatorEmpty.generate();
assertEquals(0x07, uuidEmpty.version());
assertEquals(0x02, uuidEmpty.variant());
assertEquals(minTimestamp, getTimestamp(uuidEmpty));
assertEquals(minEntropy, getEntropy(uuidEmpty));

Consumer<byte[]> entropyFull = bytes -> Arrays.fill(bytes, (byte) 0xFF);
long maxTimestamp = rightBitmask(48);
TimeBasedEpochGenerator generatorFull = new TimeBasedEpochGenerator(entropyFull, staticClock(maxTimestamp));
UUID uuidFull = generatorFull.generate();
assertEquals(0x07, uuidFull.version());
assertEquals(0x02, uuidFull.variant());
assertEquals(maxTimestamp, getTimestamp(uuidFull));
assertEquals(BigInteger.ONE.shiftLeft(73).subtract(BigInteger.ONE), getEntropy(uuidFull));
}

public void testIncrement() {
TimeBasedEpochGenerator generator = new TimeBasedEpochGenerator(staticEntropy(BigInteger.ZERO), staticClock(0));
assertEquals(BigInteger.valueOf(0), getEntropy(generator.generate()));
assertEquals(BigInteger.valueOf(1), getEntropy(generator.generate()));
assertEquals(BigInteger.valueOf(2), getEntropy(generator.generate()));
assertEquals(BigInteger.valueOf(3), getEntropy(generator.generate()));
}

public void testCarryOnce() {
TimeBasedEpochGenerator generator = new TimeBasedEpochGenerator(staticEntropy(BigInteger.valueOf(0xFF)), staticClock(0));
assertEquals(BigInteger.valueOf(0xFF), getEntropy(generator.generate()));
assertEquals(BigInteger.valueOf(0x100), getEntropy(generator.generate()));
}

public void testCarryAll() {
BigInteger largeEntropy = BigInteger.ONE.shiftLeft(73).subtract(BigInteger.ONE);
TimeBasedEpochGenerator generator = new TimeBasedEpochGenerator(staticEntropy(largeEntropy), staticClock(0));
assertEquals(largeEntropy, getEntropy(generator.generate()));
assertEquals(BigInteger.ONE.shiftLeft(73), getEntropy(generator.generate()));
}

private long getTimestamp(UUID uuid) {
return uuid.getMostSignificantBits() >>> 16;
}

private BigInteger getEntropy(UUID uuid) {
return BigInteger.valueOf(uuid.getMostSignificantBits() & rightBitmask(12)).shiftLeft(62).or(
BigInteger.valueOf(uuid.getLeastSignificantBits() & rightBitmask(62)));
}

private Consumer<byte[]> staticEntropy(BigInteger entropy) {
byte[] entropyBytes = entropy.toByteArray();
return bytes -> {
int offset = bytes.length - entropyBytes.length;
Arrays.fill(bytes, 0, offset, (byte) 0x00);
System.arraycopy(entropyBytes, 0, bytes, offset, entropyBytes.length);
};
}

private UUIDClock staticClock(long timestamp) {
return new UUIDClock() {
@Override
public long currentTimeMillis() {
return timestamp;
}
};
}

private long rightBitmask(int bits) {
return (1L << bits) - 1;
}
}