Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions bson/_cbsonmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 6 additions & 2 deletions bson/binary.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down
15 changes: 15 additions & 0 deletions doc/changelog.rst
Original file line number Diff line number Diff line change
@@ -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)
--------------------------------------

Expand Down
54 changes: 54 additions & 0 deletions test/test_bson.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@
json_util,
)
from bson.binary import (
OLD_UUID_SUBTYPE,
USER_DEFINED_SUBTYPE,
UUID_SUBTYPE,
Binary,
BinaryVector,
BinaryVectorDtype,
Expand Down Expand Up @@ -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):
Comment thread
blink1073 marked this conversation as resolved.
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.
Expand Down
Loading