spec icon indicating copy to clipboard operation
spec copied to clipboard

ULID is not strictly compatible with UUID

Open johnperry-math opened this issue 9 months ago • 4 comments

Suppose I generate a ULID, then convert it to a UUID.

Question

With which version of UUID is the result compatible?

Answer

Possibly none! Every UUID requires a variant and a version. This is also true of the original OSF DCE specification (page 586 gives the format, indicating version and variant).

A correctly-generated ULID will have random values in those fields. Thus, converting a ULID can result in a UUID that thinks its version number is anything from 0 to 15.

Why is this a problem?

I prefer to use ULID's internally, but to interact with other systems I need to generate UUID's that satisfies the requirements of a certain version. That means I'm basically out of luck, unless I want to bit-meddle. ...which I can do for the time being, but it seems like a suboptimal solution.

Proposals

  1. Add a caveat to the ULID spec that indicates this is a potential problem.
  2. Add a method that generates ULIDs compatible with a given UUID version. This probably shouldn't be allowed for UUIDs that have specific requirements.

johnperry-math avatar Mar 14 '25 19:03 johnperry-math

This is an issue I'm coming across when porting the javascript implementation of parsing from ULID to UUID. This is not UUID compatible

josetheis avatar Apr 25 '25 16:04 josetheis

I think it may be prudent to create ULID V2 which complies with the UUIDv8 spec to allow for interoperability with other systems.

CEbbinghaus avatar Apr 28 '25 07:04 CEbbinghaus

Simply generate a UUIDv7 and encode it to Crockford base32. UUIDv7 ~can be considered~ is a subset of the ULID keyspace, but not vice versa. No need to use UUIDv8 spec.

fabiolimace avatar Apr 28 '25 16:04 fabiolimace

Since UUIDv7 is ordered, it can be used instead of ULID. Because UUID is compatible with most of the systems, while ULID is not widely supported. If we need character efficient way to encode UUID to base32, below java snippet can be used.

import java.nio.ByteBuffer;
import java.util.UUID;

public class UUIDBase32Converter {

    private static final char[] BASE32_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".toCharArray();
    private static final int[] BASE32_LOOKUP = new int[128];
    static {
        for (int i = 0; i < BASE32_LOOKUP.length; i++) BASE32_LOOKUP[i] = -1;
        for (int i = 0; i < BASE32_CHARS.length; i++) BASE32_LOOKUP[BASE32_CHARS[i]] = i;
    }

    // UUID to Base32 (no padding)
    public static String uuidToBase32(UUID uuid) {
        ByteBuffer buffer = ByteBuffer.allocate(16);
        buffer.putLong(uuid.getMostSignificantBits());
        buffer.putLong(uuid.getLeastSignificantBits());
        byte[] data = buffer.array();

        StringBuilder base32 = new StringBuilder();
        int index = 0, digit = 0;
        int currByte, nextByte;

        for (int i = 0; i < data.length;) {
            currByte = data[i] & 0xFF;

            if (index > 3) {
                if (i + 1 < data.length) {
                    nextByte = (data[i + 1] >= 0) ? data[i + 1] : (data[i + 1] + 256);
                } else {
                    nextByte = 0;
                }

                digit = currByte & (0xFF >> index);
                index = (index + 5) % 8;
                digit <<= index;
                digit |= nextByte >> (8 - index);
                i++;
            } else {
                digit = (currByte >> (8 - (index + 5))) & 0x1F;
                index = (index + 5) % 8;
                if (index == 0) i++;
            }

            base32.append(BASE32_CHARS[digit]);
        }

        return base32.toString();
    }

    // Base32 to UUID
    public static UUID base32ToUUID(String base32Str) {
        String s = base32Str.toUpperCase().replace("=", "");
        byte[] bytes = new byte[16];

        int buffer = 0, bitsLeft = 0, byteIndex = 0;

        for (char c : s.toCharArray()) {
            if (c >= BASE32_LOOKUP.length || BASE32_LOOKUP[c] == -1) {
                throw new IllegalArgumentException("Invalid Base32 character: " + c);
            }

            buffer <<= 5;
            buffer |= BASE32_LOOKUP[c];
            bitsLeft += 5;

            if (bitsLeft >= 8) {
                bytes[byteIndex++] = (byte) ((buffer >> (bitsLeft - 8)) & 0xFF);
                bitsLeft -= 8;
                if (byteIndex == 16) break; // only take 16 bytes
            }
        }

        ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
        long mostSigBits = byteBuffer.getLong();
        long leastSigBits = byteBuffer.getLong();
        return new UUID(mostSigBits, leastSigBits);
    }

    public static void main(String[] args) {
        UUID originalUUID = UUID.randomUUID();
        System.out.println("Original UUID: " + originalUUID);

        String base32 = uuidToBase32(originalUUID);
        System.out.println("Base32: " + base32);

        UUID decodedUUID = base32ToUUID(base32);
        System.out.println("Decoded UUID: " + decodedUUID);

        System.out.println("Match: " + originalUUID.equals(decodedUUID));
    }
}

Example uses

UUIDBase32Converter.uuidToBase32(UUID.randomUUID());
UUUIDBase32Converter.base32ToUUID("JW4XO3YS4VANBFPIAG2R3D6U4Y");

ats1999 avatar Jul 22 '25 17:07 ats1999