diff --git a/bson/_cbsonmodule.c b/bson/_cbsonmodule.c index 40f4d9a68b..7f221f72c9 100644 --- a/bson/_cbsonmodule.c +++ b/bson/_cbsonmodule.c @@ -2351,11 +2351,6 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer, PyObject* binary_value = NULL; char uuid_rep = options->uuid_rep; - /* UUID should always be 16 bytes */ - if (length != 16) { - goto uuiderror; - } - PyObject* subtype_obj = PyLong_FromLong(subtype); if (!subtype_obj) { goto uuiderror; diff --git a/bson/binary.py b/bson/binary.py index 325f71f605..85f59db527 100644 --- a/bson/binary.py +++ b/bson/binary.py @@ -385,7 +385,7 @@ def as_uuid(self, uuid_representation: int = UuidRepresentation.STANDARD) -> UUI with the provided ``uuid_representation``. Raises :exc:`ValueError` if this :class:`~bson.binary.Binary` instance - does not contain a UUID. + does not contain a UUID, or its payload is not exactly 16 bytes. :param uuid_representation: A member of :class:`~bson.binary.UuidRepresentation`. Default: @@ -404,7 +404,11 @@ def as_uuid(self, uuid_representation: int = UuidRepresentation.STANDARD) -> UUI if uuid_representation == UuidRepresentation.UNSPECIFIED: raise ValueError("uuid_representation cannot be UNSPECIFIED") - elif uuid_representation == UuidRepresentation.PYTHON_LEGACY: + + if len(self) != 16: + raise ValueError(f"cannot decode a uuid from a {len(self)}-byte payload, expected 16") + + if uuid_representation == UuidRepresentation.PYTHON_LEGACY: if self.subtype == OLD_UUID_SUBTYPE: return UUID(bytes=self) elif uuid_representation == UuidRepresentation.JAVA_LEGACY: diff --git a/doc/changelog.rst b/doc/changelog.rst index 1ef081618d..c2b9fe2485 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -1,6 +1,21 @@ Changelog ========= +Changes in Version 4.20.0 (2026/XX/XX) +-------------------------------------- + +Bug fixes +......... + +- Fixed a disagreement between the C extension and pure-Python BSON decoders + for binary elements with UUID subtypes (3 and 4) whose payload is not 16 + bytes. Both now decode them to :class:`~bson.binary.Binary` under the + default ``UNSPECIFIED`` UUID representation, and raise + :class:`~bson.errors.InvalidBSON` when UUID conversion is requested + (`PYTHON-6172`_). + +.. _PYTHON-6172: https://jira.mongodb.org/browse/PYTHON-6172 + Changes in Version 4.19.0 (2026/10/14) -------------------------------------- diff --git a/test/test_bson.py b/test/test_bson.py index 392751ef7f..d07a9b4bb6 100644 --- a/test/test_bson.py +++ b/test/test_bson.py @@ -51,7 +51,9 @@ json_util, ) from bson.binary import ( + OLD_UUID_SUBTYPE, USER_DEFINED_SUBTYPE, + UUID_SUBTYPE, Binary, BinaryVector, BinaryVectorDtype, @@ -898,6 +900,58 @@ def test_uuid_legacy(self): transformed = bin.as_uuid(UuidRepresentation.PYTHON_LEGACY) self.assertEqual(id, transformed) + def test_short_uuid_subtype_binary(self): + # PYTHON-6172: both decoders must agree on short UUID payloads. + data = bytes.fromhex("0c0000000500000000000400") + self.assertEqual(data, encode({"": Binary(b"", UUID_SUBTYPE)})) + + # Default UNSPECIFIED representation: decode to Binary. + for subtype in (OLD_UUID_SUBTYPE, UUID_SUBTYPE): + doc = {"x": Binary(b"", subtype)} + payload = encode(doc) + self.assertEqual(decode(payload), doc) + self.assertEqual(decode_all(payload), [doc]) + self.assertTrue(is_valid(payload)) + self.assertEqual(decode_all(data), [{"": Binary(b"", UUID_SUBTYPE)}]) + + # Conversion to uuid.UUID requested: raise InvalidBSON unless the + # payload is exactly 16 bytes, overlong included. JAVA_LEGACY must + # not silently truncate non-16-byte payloads. + standard = CodecOptions(uuid_representation=UuidRepresentation.STANDARD) + converting = [(UUID_SUBTYPE, standard)] + [ + (OLD_UUID_SUBTYPE, CodecOptions(uuid_representation=rep)) + for rep in ( + UuidRepresentation.PYTHON_LEGACY, + UuidRepresentation.JAVA_LEGACY, + UuidRepresentation.CSHARP_LEGACY, + ) + ] + for length in (0, 15, 17): + for subtype, opts in converting: + payload = encode({"x": Binary(b"x" * length, subtype)}) + self.assertTrue(is_valid(payload)) + with self.assertRaisesRegex(InvalidBSON, "expected 16"): + decode_all(payload, opts) + # Mismatched subtype/representation pairs do not convert. + for rep in ( + UuidRepresentation.PYTHON_LEGACY, + UuidRepresentation.JAVA_LEGACY, + UuidRepresentation.CSHARP_LEGACY, + ): + opts = CodecOptions(uuid_representation=rep) + self.assertEqual( + decode_all(encode({"x": Binary(b"", UUID_SUBTYPE)}), opts), + [{"x": Binary(b"", UUID_SUBTYPE)}], + ) + # OLD_UUID_SUBTYPE with STANDARD is also mismatched: decode to + # Binary at any payload length, 16 included. + for length in (0, 15, 16, 17): + doc = {"x": Binary(b"x" * length, OLD_UUID_SUBTYPE)} + payload = encode(doc) + self.assertTrue(is_valid(payload)) + self.assertEqual(decode(payload, standard), doc) + self.assertEqual(decode_all(payload, standard), [doc]) + def test_vector(self): """Tests of subtype 9""" # We start with valid cases, across the 3 dtypes implemented.