Reputation: 1086
To save space when passing UUIDs with Protocol Buffers, we send them using the MSB/LSB representation, two 64-bit long
values.
message Uuid {
sfixed64 msb = 1;
sfixed64 lsb = 2;
}
These are simple to go to and from in Java,
UUID id = UUID.fromString("eb66c416-4739-465b-9af3-9dc33ed8eef9");
long msb = id.getMostSignificantBits();
long lsb = id.getLeastSignificantBits();
System.out.println(msb + ", " + lsb);
// -1484283427208739237, -7281302710629372167
System.out.println(new UUID(msb, lsb));
// eb66c416-4739-465b-9af3-9dc33ed8eef9
However, since JavaScript's number
only goes up to 253 - 1, I'm unable to convert the MSB/LSB format back to a string
in my TypeScript client. Is this possible?
Upvotes: 2
Views: 2093
Reputation: 1086
Looking at Java UUID
's toString()
method for inspiration,
public String toString() {
return (digits(mostSigBits >> 32, 8) + "-" +
digits(mostSigBits >> 16, 4) + "-" +
digits(mostSigBits, 4) + "-" +
digits(leastSigBits >> 48, 4) + "-" +
digits(leastSigBits, 12));
}
private static String digits(long val, int digits) {
long hi = 1L << (digits * 4);
return Long.toHexString(hi | (val & (hi - 1))).substring(1);
}
We can do the same using BigInt
. This assumes Node 10.8+ (tested with 18.14.0), TypeScript targeting ES2020+, and with this browser compatibility.
Note: If you get "BigInt literals are not available..." wrap all literals ending with n
with BigInt
instead (e.g., instead of 32n
, use BigInt(32)
).
function uuidSigBitsToStr({ lsb, msb }: { lsb: bigint; msb: bigint }): string {
return `${digits(msb >> 32n, 8n)}-${digits(msb >> 16n, 4n)}-${digits(
msb,
4n
)}-${digits(lsb >> 48n, 4n)}-${digits(lsb, 12n)}`;
}
function digits(val: bigint, ds: bigint): string {
const hi = 1n << (ds * 4n);
return (hi | (val & (hi - 1n))).toString(16).substring(1);
}
And an example test, notice msb/lsb are passed to BigInt
as string
s,
it('converts UUID from msb/lsb to string', () => {
expect(
toUuidString(
BigInt('-1160168401362026442'),
BigInt('-6694969989912915968')
)
).toEqual('a316b044-0157-1000-efe6-40fc5d2f0036');
});
The final piece is protocol buffers. By default, google-protobuf
uses number
for 64-bit float
and int
values, which causes overflow above Number.MAX_VALUE
or 253 - 1. To avoid this, use the jstype
annotation on 64-bit fields,
message Uuid {
sfixed64 msb = 1 [jstype = JS_STRING];
sfixed64 lsb = 2 [jstype = JS_STRING];
}
2023 update: the reverse function, convert from UUID string to significant bits
function uuidStrToSigBits(uuid: string) {
const invalidError = () => new Error(`Invalid UUID string: '${uuid}'`);
if (uuid == null || typeof uuid !== "string") throw invalidError();
const parts = uuid.split("-").map((p) => `0x${p}`);
if (parts.length !== 5) throw invalidError();
return {
lsb: (hexStrToBigInt(parts[3]) << 48n) | hexStrToBigInt(parts[4]),
msb:
(hexStrToBigInt(parts[0]) << 32n) |
(hexStrToBigInt(parts[1]) << 16n) |
hexStrToBigInt(parts[2]),
};
}
function hexStrToBigInt(hex: string): bigint {
return BigInt(Number.parseInt(hex, 16));
}
Here's a round-trip test,
const uuidSigBits = uuidStrToSigBits("7680fc63-1f58-412a-9bde-c5a9de1e0ce9");
const uuidStr = uuidSigBitsToStr(uuidSigBits);
console.log(uuidStr);
// => 7680fc63-1f58-412a-9bde-c5a9de1e0ce9
Upvotes: 3