Skip to content

Commit d310dd6

Browse files
authored
Merge branch 'master' into PYTHON-5713
2 parents ebb7b1f + 0adf6df commit d310dd6

4 files changed

Lines changed: 161 additions & 80 deletions

File tree

bson/_cbsonmodule.c

Lines changed: 145 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ struct module_state {
109109
#define DATETIME_CLAMP 2
110110
#define DATETIME_MS 3
111111
#define DATETIME_AUTO 4
112+
#define PYTHON_3_12 0x030C0000
112113

113114
/* Converts integer to its string representation in decimal notation. */
114115
extern int cbson_long_long_to_str(long long num, char* str, size_t size) {
@@ -249,6 +250,67 @@ static int _write_element_to_buffer(PyObject* self, buffer_t buffer,
249250
*/
250251
static int write_raw_doc(buffer_t buffer, PyObject* raw, PyObject* _raw);
251252

253+
#if PY_VERSION_HEX >= PYTHON_3_12
254+
/* Transfer traceback from old_exc to new_exc.
255+
* Steals reference to old_exc. */
256+
static PyObject* _transfer_traceback(PyObject *old_exc, PyObject *new_exc) {
257+
PyObject *tb = PyException_GetTraceback(old_exc);
258+
if (tb) {
259+
PyException_SetTraceback(new_exc, tb);
260+
Py_DECREF(tb);
261+
}
262+
Py_DECREF(old_exc);
263+
return new_exc;
264+
}
265+
#endif
266+
267+
/* Rewrap the current exception as InvalidBSON(str(e)) if it is not already an InvalidBSON error. */
268+
static void _rewrap_as_invalid_bson(void) {
269+
#if PY_VERSION_HEX >= PYTHON_3_12
270+
PyObject *exc = PyErr_GetRaisedException();
271+
if (exc && PyErr_GivenExceptionMatches(exc, PyExc_Exception)) {
272+
PyObject *InvalidBSON = _error("InvalidBSON");
273+
if (InvalidBSON) {
274+
if (!PyErr_GivenExceptionMatches(exc, InvalidBSON)) {
275+
PyObject *err_msg = PyObject_Str(exc);
276+
if (err_msg) {
277+
PyObject *new_exc = PyObject_CallOneArg(InvalidBSON, err_msg);
278+
if (new_exc) {
279+
exc = _transfer_traceback(exc, new_exc);
280+
}
281+
}
282+
Py_XDECREF(err_msg);
283+
}
284+
Py_DECREF(InvalidBSON);
285+
}
286+
}
287+
/* Steals reference to exc. */
288+
PyErr_SetRaisedException(exc);
289+
#else
290+
PyObject *etype = NULL, *evalue = NULL, *etrace = NULL;
291+
PyObject *InvalidBSON = NULL;
292+
PyErr_Fetch(&etype, &evalue, &etrace);
293+
if (PyErr_GivenExceptionMatches(etype, PyExc_Exception)) {
294+
InvalidBSON = _error("InvalidBSON");
295+
if (InvalidBSON) {
296+
if (!PyErr_GivenExceptionMatches(etype, InvalidBSON)) {
297+
Py_DECREF(etype);
298+
etype = InvalidBSON;
299+
if (evalue) {
300+
PyObject *msg = PyObject_Str(evalue);
301+
Py_DECREF(evalue);
302+
evalue = msg;
303+
}
304+
PyErr_NormalizeException(&etype, &evalue, &etrace);
305+
} else {
306+
Py_DECREF(InvalidBSON);
307+
}
308+
}
309+
}
310+
PyErr_Restore(etype, evalue, etrace);
311+
#endif
312+
}
313+
252314
/* Date stuff */
253315
static PyObject* datetime_from_millis(long long millis) {
254316
/* To encode a datetime instance like datetime(9999, 12, 31, 23, 59, 59, 999999)
@@ -294,34 +356,57 @@ static PyObject* datetime_from_millis(long long millis) {
294356
timeinfo.tm_sec,
295357
microseconds);
296358
if(!datetime) {
297-
PyObject *etype = NULL, *evalue = NULL, *etrace = NULL;
359+
#if PY_VERSION_HEX >= PYTHON_3_12
360+
PyObject *exc = PyErr_GetRaisedException();
298361

299-
/*
300-
* Calling _error clears the error state, so fetch it first.
301-
*/
302-
PyErr_Fetch(&etype, &evalue, &etrace);
303-
304-
/* Only add addition error message on ValueError exceptions. */
305-
if (PyErr_GivenExceptionMatches(etype, PyExc_ValueError)) {
306-
if (evalue) {
307-
PyObject* err_msg = PyObject_Str(evalue);
362+
/* Only add additional error message on ValueError exceptions. */
363+
if (exc && PyErr_GivenExceptionMatches(exc, PyExc_ValueError)) {
364+
PyObject* err_msg = PyObject_Str(exc);
308365
if (err_msg) {
309366
PyObject* appendage = PyUnicode_FromString(" (Consider Using CodecOptions(datetime_conversion=DATETIME_AUTO) or MongoClient(datetime_conversion='DATETIME_AUTO')). See: https://www.mongodb.com/docs/languages/python/pymongo-driver/current/data-formats/dates-and-times/#handling-out-of-range-datetimes");
310367
if (appendage) {
311368
PyObject* msg = PyUnicode_Concat(err_msg, appendage);
312369
if (msg) {
313-
Py_DECREF(evalue);
314-
evalue = msg;
370+
PyObject* new_exc = PyObject_CallOneArg(PyExc_ValueError, msg);
371+
if (new_exc) {
372+
exc = _transfer_traceback(exc, new_exc);
373+
}
374+
Py_DECREF(msg);
315375
}
316376
}
317377
Py_XDECREF(appendage);
318378
}
319379
Py_XDECREF(err_msg);
320380
}
321-
PyErr_NormalizeException(&etype, &evalue, &etrace);
322-
}
323-
/* Steals references to args. */
324-
PyErr_Restore(etype, evalue, etrace);
381+
/* Steals reference to exc. */
382+
PyErr_SetRaisedException(exc);
383+
#else
384+
/* Calling _error clears the error state, so fetch it first.*/
385+
PyObject *etype = NULL, *evalue = NULL, *etrace = NULL;
386+
PyErr_Fetch(&etype, &evalue, &etrace);
387+
388+
/* Only add additional error message on ValueError exceptions. */
389+
if (PyErr_GivenExceptionMatches(etype, PyExc_ValueError)) {
390+
if (evalue) {
391+
PyObject* err_msg = PyObject_Str(evalue);
392+
if (err_msg) {
393+
PyObject* appendage = PyUnicode_FromString(" (Consider Using CodecOptions(datetime_conversion=DATETIME_AUTO) or MongoClient(datetime_conversion='DATETIME_AUTO')). See: https://www.mongodb.com/docs/languages/python/pymongo-driver/current/data-formats/dates-and-times/#handling-out-of-range-datetimes");
394+
if (appendage) {
395+
PyObject* msg = PyUnicode_Concat(err_msg, appendage);
396+
if (msg) {
397+
Py_DECREF(evalue);
398+
evalue = msg;
399+
}
400+
}
401+
Py_XDECREF(appendage);
402+
}
403+
Py_XDECREF(err_msg);
404+
}
405+
PyErr_NormalizeException(&etype, &evalue, &etrace);
406+
}
407+
/* Steals references to args. */
408+
PyErr_Restore(etype, evalue, etrace);
409+
#endif
325410
}
326411
return datetime;
327412
}
@@ -1681,6 +1766,46 @@ static int write_raw_doc(buffer_t buffer, PyObject* raw, PyObject* _raw_str) {
16811766
/* Update Invalid Document error to include doc as a property.
16821767
*/
16831768
void handle_invalid_doc_error(PyObject* dict) {
1769+
#if PY_VERSION_HEX >= PYTHON_3_12
1770+
PyObject *exc = PyErr_GetRaisedException();
1771+
PyObject *msg = NULL, *new_msg = NULL;
1772+
PyObject *InvalidDocument = NULL;
1773+
1774+
if (exc == NULL) {
1775+
return;
1776+
}
1777+
1778+
InvalidDocument = _error("InvalidDocument");
1779+
if (InvalidDocument == NULL) {
1780+
goto cleanup;
1781+
}
1782+
1783+
if (PyErr_GivenExceptionMatches(exc, InvalidDocument)) {
1784+
msg = PyObject_Str(exc);
1785+
if (msg) {
1786+
const char *msg_utf8 = PyUnicode_AsUTF8(msg);
1787+
if (msg_utf8 == NULL) {
1788+
goto cleanup;
1789+
}
1790+
new_msg = PyUnicode_FromFormat("Invalid document: %s", msg_utf8);
1791+
if (new_msg == NULL) {
1792+
goto cleanup;
1793+
}
1794+
/* Add doc to the error instance as a property. */
1795+
PyObject* exc_args[2] = {new_msg, dict};
1796+
PyObject* new_exc = PyObject_Vectorcall(InvalidDocument, exc_args, 2, NULL);
1797+
if (new_exc) {
1798+
exc = _transfer_traceback(exc, new_exc);
1799+
}
1800+
}
1801+
}
1802+
cleanup:
1803+
/* Steals reference to exc. */
1804+
PyErr_SetRaisedException(exc);
1805+
Py_XDECREF(msg);
1806+
Py_XDECREF(InvalidDocument);
1807+
Py_XDECREF(new_msg);
1808+
#else
16841809
PyObject *etype = NULL, *evalue = NULL, *etrace = NULL;
16851810
PyObject *msg = NULL, *new_msg = NULL, *new_evalue = NULL;
16861811
PyErr_Fetch(&etype, &evalue, &etrace);
@@ -1723,6 +1848,7 @@ void handle_invalid_doc_error(PyObject* dict) {
17231848
Py_XDECREF(InvalidDocument);
17241849
Py_XDECREF(new_evalue);
17251850
Py_XDECREF(new_msg);
1851+
#endif
17261852
}
17271853

17281854

@@ -2155,7 +2281,7 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
21552281
}
21562282
memcpy(&length, buffer + *position, 4);
21572283
length = BSON_UINT32_FROM_LE(length);
2158-
if (max < length) {
2284+
if (max - 5 < length) { // Account for 5-byte header. max >= 5 guaranteed above
21592285
goto invalid;
21602286
}
21612287

@@ -2654,42 +2780,7 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
26542780
* Wrap any non-InvalidBSON errors in InvalidBSON.
26552781
*/
26562782
if (PyErr_Occurred()) {
2657-
PyObject *etype = NULL, *evalue = NULL, *etrace = NULL;
2658-
PyObject *InvalidBSON = NULL;
2659-
2660-
/*
2661-
* Calling _error clears the error state, so fetch it first.
2662-
*/
2663-
PyErr_Fetch(&etype, &evalue, &etrace);
2664-
2665-
/* Dont reraise anything but PyExc_Exceptions as InvalidBSON. */
2666-
if (PyErr_GivenExceptionMatches(etype, PyExc_Exception)) {
2667-
InvalidBSON = _error("InvalidBSON");
2668-
if (InvalidBSON) {
2669-
if (!PyErr_GivenExceptionMatches(etype, InvalidBSON)) {
2670-
/*
2671-
* Raise InvalidBSON(str(e)).
2672-
*/
2673-
Py_DECREF(etype);
2674-
etype = InvalidBSON;
2675-
2676-
if (evalue) {
2677-
PyObject *msg = PyObject_Str(evalue);
2678-
Py_DECREF(evalue);
2679-
evalue = msg;
2680-
}
2681-
PyErr_NormalizeException(&etype, &evalue, &etrace);
2682-
} else {
2683-
/*
2684-
* The current exception matches InvalidBSON, so we don't
2685-
* need this reference after all.
2686-
*/
2687-
Py_DECREF(InvalidBSON);
2688-
}
2689-
}
2690-
}
2691-
/* Steals references to args. */
2692-
PyErr_Restore(etype, evalue, etrace);
2783+
_rewrap_as_invalid_bson();
26932784
} else {
26942785
PyObject *InvalidBSON = _error("InvalidBSON");
26952786
if (InvalidBSON) {
@@ -2727,25 +2818,7 @@ static int _element_to_dict(PyObject* self, const char* string,
27272818
if (!*name) {
27282819
/* If NULL is returned then wrap the UnicodeDecodeError
27292820
in an InvalidBSON error */
2730-
PyObject *etype = NULL, *evalue = NULL, *etrace = NULL;
2731-
PyObject *InvalidBSON = NULL;
2732-
2733-
PyErr_Fetch(&etype, &evalue, &etrace);
2734-
if (PyErr_GivenExceptionMatches(etype, PyExc_Exception)) {
2735-
InvalidBSON = _error("InvalidBSON");
2736-
if (InvalidBSON) {
2737-
Py_DECREF(etype);
2738-
etype = InvalidBSON;
2739-
2740-
if (evalue) {
2741-
PyObject *msg = PyObject_Str(evalue);
2742-
Py_DECREF(evalue);
2743-
evalue = msg;
2744-
}
2745-
PyErr_NormalizeException(&etype, &evalue, &etrace);
2746-
}
2747-
}
2748-
PyErr_Restore(etype, evalue, etrace);
2821+
_rewrap_as_invalid_bson();
27492822
return -1;
27502823
}
27512824
position += (unsigned)name_length + 1;

test/asynchronous/test_encryption.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -876,8 +876,6 @@ async def test_views_are_prohibited(self):
876876

877877

878878
class TestCorpus(AsyncEncryptionIntegrationTest):
879-
# PYTHON-5708: Encryption tests sending large payloads fail on some mongocryptd versions.
880-
@async_client_context.require_version_max(6, 99)
881879
@unittest.skipUnless(any(AWS_CREDS.values()), "AWS environment credentials are not set")
882880
async def asyncSetUp(self):
883881
await super().asyncSetUp()
@@ -1054,8 +1052,6 @@ class TestBsonSizeBatches(AsyncEncryptionIntegrationTest):
10541052
client_encrypted: AsyncMongoClient
10551053
listener: OvertCommandListener
10561054

1057-
# PYTHON-5708: Encryption tests sending large payloads fail on some mongocryptd versions.
1058-
@async_client_context.require_version_max(6, 99)
10591055
async def asyncSetUp(self):
10601056
await super().asyncSetUp()
10611057
db = async_client_context.client.db

test/test_bson.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1269,6 +1269,22 @@ def __repr__(self):
12691269
encode(doc)
12701270
self.assertEqual(cm.exception.document, doc)
12711271

1272+
def test_binary_length_accounts_for_header(self):
1273+
size = 20
1274+
binary_length = 12 # 5 more than the actual 7 bytes
1275+
1276+
payload = b""
1277+
payload += struct.pack("<i", size) # document size
1278+
payload += b"\x05" # type = Binary
1279+
payload += b"a\x00" # key "a"
1280+
payload += struct.pack("<I", binary_length) # Binary length (inflated)
1281+
payload += b"\x00" # subtype 0
1282+
payload += b"\x41" * 7 # value
1283+
payload += b"\x00" # EOO
1284+
1285+
with self.assertRaises(InvalidBSON):
1286+
decode(payload)
1287+
12721288

12731289
class TestCodecOptions(unittest.TestCase):
12741290
def test_document_class(self):

test/test_encryption.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -872,8 +872,6 @@ def test_views_are_prohibited(self):
872872

873873

874874
class TestCorpus(EncryptionIntegrationTest):
875-
# PYTHON-5708: Encryption tests sending large payloads fail on some mongocryptd versions.
876-
@client_context.require_version_max(6, 99)
877875
@unittest.skipUnless(any(AWS_CREDS.values()), "AWS environment credentials are not set")
878876
def setUp(self):
879877
super().setUp()
@@ -1050,8 +1048,6 @@ class TestBsonSizeBatches(EncryptionIntegrationTest):
10501048
client_encrypted: MongoClient
10511049
listener: OvertCommandListener
10521050

1053-
# PYTHON-5708: Encryption tests sending large payloads fail on some mongocryptd versions.
1054-
@client_context.require_version_max(6, 99)
10551051
def setUp(self):
10561052
super().setUp()
10571053
db = client_context.client.db

0 commit comments

Comments
 (0)