@@ -349,23 +349,31 @@ where
349349 . ok_or_else ( || anyhow ! ( "end of data" ) )
350350}
351351
352+ // Snapshot `sys.getrecursionlimit()` once per top-level decode call and pass
353+ // it through. Calling `ffi::Py_GetRecursionLimit()` from the hot path costs
354+ // ~5–10 ns per recursive step, which dominates on scalar-dense payloads
355+ // (canada makes 111k+ recursive calls, one per float).
356+ #[ inline]
357+ fn current_recursion_limit ( ) -> usize {
358+ unsafe { ffi:: Py_GetRecursionLimit ( ) as usize }
359+ }
360+
352361fn decode_dag_cbor_to_pyobject < ' de , R : dec:: Read < ' de > > (
353362 py : Python ,
354363 r : & mut R ,
355364 depth : usize ,
365+ max_depth : usize ,
356366) -> Result < Py < PyAny > >
357367where
358368 R :: Error : Send + Sync ,
359369{
360- unsafe {
361- if depth > ffi:: Py_GetRecursionLimit ( ) as usize {
362- PyErr :: new :: < pyo3:: exceptions:: PyRecursionError , _ > (
363- "RecursionError: maximum recursion depth exceeded in DAG-CBOR decoding" ,
364- )
365- . restore ( py) ;
370+ if depth > max_depth {
371+ PyErr :: new :: < pyo3:: exceptions:: PyRecursionError , _ > (
372+ "RecursionError: maximum recursion depth exceeded in DAG-CBOR decoding" ,
373+ )
374+ . restore ( py) ;
366375
367- return Err ( anyhow ! ( "Maximum recursion depth exceeded" ) ) ;
368- }
376+ return Err ( anyhow ! ( "Maximum recursion depth exceeded" ) ) ;
369377 }
370378
371379 let byte = peek_one ( r) ?;
@@ -398,7 +406,7 @@ where
398406 ffi:: PyList_SET_ITEM (
399407 ptr,
400408 i,
401- decode_dag_cbor_to_pyobject ( py, r, depth + 1 ) ?. into_ptr ( ) ,
409+ decode_dag_cbor_to_pyobject ( py, r, depth + 1 , max_depth ) ?. into_ptr ( ) ,
402410 ) ;
403411 }
404412
@@ -437,7 +445,7 @@ where
437445 let ( key_ptr, key_hash) = unsafe { key_cache:: intern_key ( py, key) ? } ;
438446 let key_bound: Bound < ' _ , PyAny > = unsafe { Bound :: from_owned_ptr ( py, key_ptr) } ;
439447
440- let value_py = decode_dag_cbor_to_pyobject ( py, r, depth + 1 ) ?;
448+ let value_py = decode_dag_cbor_to_pyobject ( py, r, depth + 1 , max_depth ) ?;
441449
442450 #[ cfg( CPython ) ]
443451 unsafe {
@@ -714,9 +722,10 @@ where
714722fn decode_dag_cbor_multi < ' py > ( py : Python < ' py > , data : & [ u8 ] ) -> PyResult < Bound < ' py , PyList > > {
715723 let mut reader = SliceReader :: new ( data) ;
716724 let decoded_parts = PyList :: empty ( py) ;
725+ let max_depth = current_recursion_limit ( ) ;
717726
718727 loop {
719- let py_object = decode_dag_cbor_to_pyobject ( py, & mut reader, 0 ) ;
728+ let py_object = decode_dag_cbor_to_pyobject ( py, & mut reader, 0 , max_depth ) ;
720729 if let Ok ( py_object) = py_object {
721730 decoded_parts. append ( py_object) ?;
722731 } else {
@@ -766,14 +775,15 @@ where
766775#[ pyfunction]
767776pub fn decode_car < ' py > ( py : Python < ' py > , data : & [ u8 ] ) -> PyResult < ( Py < PyAny > , Bound < ' py , PyDict > ) > {
768777 let buf = & mut SliceReader :: new ( data) ;
778+ let max_depth = current_recursion_limit ( ) ;
769779
770780 if read_u64_leb128 ( buf) . is_err ( ) {
771781 return Err ( get_err (
772782 "Failed to read CAR header" ,
773783 "Invalid uvarint" . to_string ( ) ,
774784 ) ) ;
775785 }
776- let Ok ( header_obj) = decode_dag_cbor_to_pyobject ( py, buf, 0 ) else {
786+ let Ok ( header_obj) = decode_dag_cbor_to_pyobject ( py, buf, 0 , max_depth ) else {
777787 return Err ( get_err (
778788 "Failed to read CAR header" ,
779789 "Invalid DAG-CBOR" . to_string ( ) ,
@@ -841,7 +851,7 @@ pub fn decode_car<'py>(py: Python<'py>, data: &[u8]) -> PyResult<(Py<PyAny>, Bou
841851 buf. advance ( consumed) ;
842852 let cid_raw = & cid_bytes_before[ ..consumed] ;
843853
844- let block_result = decode_dag_cbor_to_pyobject ( py, buf, 0 ) ;
854+ let block_result = decode_dag_cbor_to_pyobject ( py, buf, 0 , max_depth ) ;
845855 let Ok ( block) = block_result else {
846856 return Err ( get_err (
847857 "Failed to read CAR block" ,
@@ -859,7 +869,8 @@ pub fn decode_car<'py>(py: Python<'py>, data: &[u8]) -> PyResult<(Py<PyAny>, Bou
859869#[ pyfunction]
860870pub fn decode_dag_cbor ( py : Python , data : & [ u8 ] ) -> PyResult < Py < PyAny > > {
861871 let mut reader = SliceReader :: new ( data) ;
862- let py_object = decode_dag_cbor_to_pyobject ( py, & mut reader, 0 ) ;
872+ let max_depth = current_recursion_limit ( ) ;
873+ let py_object = decode_dag_cbor_to_pyobject ( py, & mut reader, 0 , max_depth) ;
863874 if let Ok ( py_object) = py_object {
864875 // check for any remaining data in the reader
865876 if reader. fill ( 1 ) ?. as_ref ( ) . is_empty ( ) {
0 commit comments