From 2a9f043039dc60fc25bc14bab724419bedf746bb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 May 2022 23:44:54 -0500 Subject: [PATCH] Use ULID short format for context ids (#71119) --- homeassistant/core.py | 2 +- homeassistant/util/ulid.py | 63 +++++++++++++++++++++++++++++++++----- tests/util/test_ulid.py | 7 ++++- 3 files changed, 63 insertions(+), 9 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 7245dad7864..d6b1ac85d8b 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -695,7 +695,7 @@ class Context: user_id: str | None = attr.ib(default=None) parent_id: str | None = attr.ib(default=None) - id: str = attr.ib(factory=ulid_util.ulid_hex) + id: str = attr.ib(factory=ulid_util.ulid) def as_dict(self) -> dict[str, str | None]: """Return a dictionary representation of the context.""" diff --git a/homeassistant/util/ulid.py b/homeassistant/util/ulid.py index 888b5b2074b..c38d95bd169 100644 --- a/homeassistant/util/ulid.py +++ b/homeassistant/util/ulid.py @@ -4,11 +4,21 @@ from random import getrandbits import time -# In the future once we require python 3.10 and above, we can -# create a new function that uses base64.b32encodehex to shorten -# these to 26 characters. def ulid_hex() -> str: - """Generate a ULID in hex that will work for a UUID. + """Generate a ULID in lowercase hex that will work for a UUID. + + This ulid should not be used for cryptographically secure + operations. + + This string can be converted with https://github.com/ahawker/ulid + + ulid.from_uuid(uuid.UUID(ulid_hex)) + """ + return f"{int(time.time()*1000):012x}{getrandbits(80):020x}" + + +def ulid() -> str: + """Generate a ULID. This ulid should not be used for cryptographically secure operations. @@ -18,8 +28,47 @@ def ulid_hex() -> str: Timestamp Randomness 48bits 80bits - This string can be converted with https://github.com/ahawker/ulid + This string can be loaded directly with https://github.com/ahawker/ulid - ulid.from_uuid(uuid.UUID(value)) + import homeassistant.util.ulid as ulid_util + import ulid + ulid.parse(ulid_util.ulid()) """ - return f"{int(time.time()*1000):012x}{getrandbits(80):020x}" + ulid_bytes = int(time.time() * 1000).to_bytes(6, byteorder="big") + int( + getrandbits(80) + ).to_bytes(10, byteorder="big") + + # This is base32 crockford encoding with the loop unrolled for performance + # + # This code is adapted from: + # https://github.com/ahawker/ulid/blob/06289583e9de4286b4d80b4ad000d137816502ca/ulid/base32.py#L102 + # + enc = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" + return ( + enc[(ulid_bytes[0] & 224) >> 5] + + enc[ulid_bytes[0] & 31] + + enc[(ulid_bytes[1] & 248) >> 3] + + enc[((ulid_bytes[1] & 7) << 2) | ((ulid_bytes[2] & 192) >> 6)] + + enc[((ulid_bytes[2] & 62) >> 1)] + + enc[((ulid_bytes[2] & 1) << 4) | ((ulid_bytes[3] & 240) >> 4)] + + enc[((ulid_bytes[3] & 15) << 1) | ((ulid_bytes[4] & 128) >> 7)] + + enc[(ulid_bytes[4] & 124) >> 2] + + enc[((ulid_bytes[4] & 3) << 3) | ((ulid_bytes[5] & 224) >> 5)] + + enc[ulid_bytes[5] & 31] + + enc[(ulid_bytes[6] & 248) >> 3] + + enc[((ulid_bytes[6] & 7) << 2) | ((ulid_bytes[7] & 192) >> 6)] + + enc[(ulid_bytes[7] & 62) >> 1] + + enc[((ulid_bytes[7] & 1) << 4) | ((ulid_bytes[8] & 240) >> 4)] + + enc[((ulid_bytes[8] & 15) << 1) | ((ulid_bytes[9] & 128) >> 7)] + + enc[(ulid_bytes[9] & 124) >> 2] + + enc[((ulid_bytes[9] & 3) << 3) | ((ulid_bytes[10] & 224) >> 5)] + + enc[ulid_bytes[10] & 31] + + enc[(ulid_bytes[11] & 248) >> 3] + + enc[((ulid_bytes[11] & 7) << 2) | ((ulid_bytes[12] & 192) >> 6)] + + enc[(ulid_bytes[12] & 62) >> 1] + + enc[((ulid_bytes[12] & 1) << 4) | ((ulid_bytes[13] & 240) >> 4)] + + enc[((ulid_bytes[13] & 15) << 1) | ((ulid_bytes[14] & 128) >> 7)] + + enc[(ulid_bytes[14] & 124) >> 2] + + enc[((ulid_bytes[14] & 3) << 3) | ((ulid_bytes[15] & 224) >> 5)] + + enc[ulid_bytes[15] & 31] + ) diff --git a/tests/util/test_ulid.py b/tests/util/test_ulid.py index fc5d2af8c87..9f297bf373e 100644 --- a/tests/util/test_ulid.py +++ b/tests/util/test_ulid.py @@ -6,6 +6,11 @@ import homeassistant.util.ulid as ulid_util async def test_ulid_util_uuid_hex(): - """Verify we can generate a ulid.""" + """Verify we can generate a ulid in hex.""" assert len(ulid_util.ulid_hex()) == 32 assert uuid.UUID(ulid_util.ulid_hex()) + + +async def test_ulid_util_uuid(): + """Verify we can generate a ulid.""" + assert len(ulid_util.ulid()) == 26