Skip to content

Commit

Permalink
fix: ensure the timestamp validation checks the upper limit given in …
Browse files Browse the repository at this point in the history
…the `ulid` spec (#27)
  • Loading branch information
somnam authored Jun 17, 2024
1 parent 419116f commit 1d5ed28
Show file tree
Hide file tree
Showing 5 changed files with 45 additions and 8 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ Changelog

Versions follow `Semantic Versioning <http://www.semver.org>`_

`2.7.0`_ - 2024-06-16
---------------------
Changed
~~~~~~~
* Ensure that the validation of ULID's timestamp component aligns more closely with
the ULID specification.

`2.6.0`_ - 2024-05-26
---------------------
Changed
Expand Down Expand Up @@ -167,6 +174,7 @@ Changed
* The package now has no external dependencies.
* The test-coverage has been raised to 100%.

.. _2.7.0: https://github.com/mdomke/python-ulid/compare/2.6.0...2.7.0
.. _2.6.0: https://github.com/mdomke/python-ulid/compare/2.5.0...2.6.0
.. _2.5.0: https://github.com/mdomke/python-ulid/compare/2.4.0...2.5.0
.. _2.4.0: https://github.com/mdomke/python-ulid/compare/2.3.0...2.4.0
Expand Down
1 change: 1 addition & 0 deletions tests/test_base32.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
(base32.decode, "A" * (constants.REPR_LEN + 1)),
(base32.decode_timestamp, "A" * (constants.TIMESTAMP_REPR_LEN - 1)),
(base32.decode_timestamp, "A" * (constants.TIMESTAMP_REPR_LEN + 1)),
(base32.decode_timestamp, "Z" * constants.TIMESTAMP_REPR_LEN),
(base32.decode_randomness, "A" * (constants.RANDOMNESS_REPR_LEN - 1)),
(base32.decode_randomness, "A" * (constants.RANDOMNESS_REPR_LEN + 1)),
],
Expand Down
31 changes: 31 additions & 0 deletions tests/test_ulid.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,37 @@ def test_ulid_invalid_input(constructor: Callable[[Params], ULID], value: Params
constructor(value)


@pytest.mark.parametrize(
("constructor", "value"),
[
(ULID, b"\x00" * 16),
(ULID.from_timestamp, 0),
(ULID.from_bytes, b"\x00" * 16),
(ULID.from_str, "0" * 26),
(ULID.from_hex, "0" * 32),
(ULID.from_uuid, uuid.UUID("0" * 32)),
],
)
def test_ulid_min_input(constructor: Callable[[Params], ULID], value: Params) -> None:
constructor(value)


@pytest.mark.parametrize(
("constructor", "value"),
[
(ULID, b"\xff" * 16),
(ULID.from_timestamp, 281474976710655),
(ULID.from_datetime, datetime.max.replace(tzinfo=timezone.utc)),
(ULID.from_bytes, b"\xff" * 16),
(ULID.from_str, "7" + "Z" * 25),
(ULID.from_hex, "f" * 32),
(ULID.from_uuid, uuid.UUID("f" * 32)),
],
)
def test_ulid_max_input(constructor: Callable[[Params], ULID], value: Params) -> None:
constructor(value)


def test_pydantic_protocol() -> None:
ulid = ULID()

Expand Down
10 changes: 2 additions & 8 deletions ulid/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,23 +74,17 @@ class ULID:
Args:
value (bytes, None): A sequence of 16 bytes representing an encoded ULID.
validate (bool): If set to `True` validate if the timestamp part is valid.
Raises:
ValueError: If the provided value is not a valid encoded ULID.
"""

def __init__(self, value: bytes | None = None, validate: bool = True) -> None:
def __init__(self, value: bytes | None = None) -> None:
if value is not None and len(value) != constants.BYTES_LEN:
raise ValueError("ULID has to be exactly 16 bytes long.")
self.bytes: bytes = (
value or ULID.from_timestamp(time.time_ns() // constants.NANOSECS_IN_MILLISECS).bytes
)
if value is not None and validate:
try:
self.datetime # noqa: B018
except ValueError as err:
raise ValueError("ULID timestamp is out of range.") from err

@classmethod
@validate_type(datetime)
Expand Down Expand Up @@ -137,7 +131,7 @@ def from_uuid(cls: type[U], value: uuid.UUID) -> U:
>>> ULID.from_uuid(uuid4())
ULID(27Q506DP7E9YNRXA0XVD8Z5YSG)
"""
return cls(value.bytes, validate=False)
return cls(value.bytes)

@classmethod
@validate_type(bytes)
Expand Down
3 changes: 3 additions & 0 deletions ulid/base32.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,9 @@ def decode_timestamp(encoded: str) -> bytes:
raise ValueError("ULID timestamp has to be exactly 10 characters long.")
lut = DECODE
values: bytes = bytes(encoded, "ascii")
# https://github.com/ulid/spec?tab=readme-ov-file#overflow-errors-when-parsing-base32-strings
if lut[values[0]] > 7:
raise ValueError(f"Timestamp value {encoded} is too large and will overflow 128-bits.")
return bytes(
[
((lut[values[0]] << 5) | lut[values[1]]) & 0xFF,
Expand Down

0 comments on commit 1d5ed28

Please sign in to comment.