6
6
7
7
from collections .abc import Iterable
8
8
import string
9
+ import sys
9
10
from types import MappingProxyType
10
- from typing import IO , Any , NamedTuple
11
+ from typing import IO , Any , Final , NamedTuple
11
12
import warnings
12
13
13
14
from ._re import (
20
21
)
21
22
from ._types import Key , ParseFloat , Pos
22
23
24
+ # Inline tables/arrays are implemented using recursion. Pathologically
25
+ # nested documents cause pure Python to raise RecursionError (which is OK),
26
+ # but mypyc binary wheels will crash unrecoverably (not OK). According to
27
+ # mypyc docs this will be fixed in the future:
28
+ # https://mypyc.readthedocs.io/en/latest/differences_from_python.html#stack-overflows
29
+ # Before mypyc's fix is in, recursion needs to be limited by this library.
30
+ # Choosing `sys.getrecursionlimit()` as maximum inline table/array nesting
31
+ # level, as it allows more nesting than pure Python, but still seems a far
32
+ # lower number than where mypyc binaries crash.
33
+ MAX_INLINE_NESTING : Final = sys .getrecursionlimit ()
34
+
23
35
ASCII_CTRL = frozenset (chr (i ) for i in range (32 )) | frozenset (chr (127 ))
24
36
25
37
# Neither of these sets include quotation mark or backslash. They are
@@ -69,9 +81,9 @@ class TOMLDecodeError(ValueError):
69
81
70
82
def __init__ (
71
83
self ,
72
- msg : str = DEPRECATED_DEFAULT , # type: ignore[assignment]
73
- doc : str = DEPRECATED_DEFAULT , # type: ignore[assignment]
74
- pos : Pos = DEPRECATED_DEFAULT , # type: ignore[assignment]
84
+ msg : str | type [ DEPRECATED_DEFAULT ] = DEPRECATED_DEFAULT ,
85
+ doc : str | type [ DEPRECATED_DEFAULT ] = DEPRECATED_DEFAULT ,
86
+ pos : Pos | type [ DEPRECATED_DEFAULT ] = DEPRECATED_DEFAULT ,
75
87
* args : Any ,
76
88
):
77
89
if (
@@ -86,11 +98,11 @@ def __init__(
86
98
DeprecationWarning ,
87
99
stacklevel = 2 ,
88
100
)
89
- if pos is not DEPRECATED_DEFAULT : # type: ignore[comparison-overlap]
101
+ if pos is not DEPRECATED_DEFAULT :
90
102
args = pos , * args
91
- if doc is not DEPRECATED_DEFAULT : # type: ignore[comparison-overlap]
103
+ if doc is not DEPRECATED_DEFAULT :
92
104
args = doc , * args
93
- if msg is not DEPRECATED_DEFAULT : # type: ignore[comparison-overlap]
105
+ if msg is not DEPRECATED_DEFAULT :
94
106
args = msg , * args
95
107
ValueError .__init__ (self , * args )
96
108
return
@@ -202,10 +214,10 @@ class Flags:
202
214
"""Flags that map to parsed keys/namespaces."""
203
215
204
216
# Marks an immutable namespace (inline array or inline table).
205
- FROZEN = 0
217
+ FROZEN : Final = 0
206
218
# Marks a nest that has been explicitly created and can no longer
207
219
# be opened using the "[table]" syntax.
208
- EXPLICIT_NEST = 1
220
+ EXPLICIT_NEST : Final = 1
209
221
210
222
def __init__ (self ) -> None :
211
223
self ._flags : dict [str , dict ] = {}
@@ -251,8 +263,8 @@ def is_(self, key: Key, flag: int) -> bool:
251
263
cont = inner_cont ["nested" ]
252
264
key_stem = key [- 1 ]
253
265
if key_stem in cont :
254
- cont = cont [key_stem ]
255
- return flag in cont ["flags" ] or flag in cont ["recursive_flags" ]
266
+ inner_cont = cont [key_stem ]
267
+ return flag in inner_cont ["flags" ] or flag in inner_cont ["recursive_flags" ]
256
268
return False
257
269
258
270
@@ -393,7 +405,7 @@ def create_list_rule(src: str, pos: Pos, out: Output) -> tuple[Pos, Key]:
393
405
def key_value_rule (
394
406
src : str , pos : Pos , out : Output , header : Key , parse_float : ParseFloat
395
407
) -> Pos :
396
- pos , key , value = parse_key_value_pair (src , pos , parse_float )
408
+ pos , key , value = parse_key_value_pair (src , pos , parse_float , nest_lvl = 0 )
397
409
key_parent , key_stem = key [:- 1 ], key [- 1 ]
398
410
abs_key_parent = header + key_parent
399
411
@@ -425,7 +437,7 @@ def key_value_rule(
425
437
426
438
427
439
def parse_key_value_pair (
428
- src : str , pos : Pos , parse_float : ParseFloat
440
+ src : str , pos : Pos , parse_float : ParseFloat , nest_lvl : int
429
441
) -> tuple [Pos , Key , Any ]:
430
442
pos , key = parse_key (src , pos )
431
443
try :
@@ -436,7 +448,7 @@ def parse_key_value_pair(
436
448
raise TOMLDecodeError ("Expected '=' after a key in a key/value pair" , src , pos )
437
449
pos += 1
438
450
pos = skip_chars (src , pos , TOML_WS )
439
- pos , value = parse_value (src , pos , parse_float )
451
+ pos , value = parse_value (src , pos , parse_float , nest_lvl )
440
452
return pos , key , value
441
453
442
454
@@ -479,15 +491,17 @@ def parse_one_line_basic_str(src: str, pos: Pos) -> tuple[Pos, str]:
479
491
return parse_basic_str (src , pos , multiline = False )
480
492
481
493
482
- def parse_array (src : str , pos : Pos , parse_float : ParseFloat ) -> tuple [Pos , list ]:
494
+ def parse_array (
495
+ src : str , pos : Pos , parse_float : ParseFloat , nest_lvl : int
496
+ ) -> tuple [Pos , list ]:
483
497
pos += 1
484
498
array : list = []
485
499
486
500
pos = skip_comments_and_array_ws (src , pos )
487
501
if src .startswith ("]" , pos ):
488
502
return pos + 1 , array
489
503
while True :
490
- pos , val = parse_value (src , pos , parse_float )
504
+ pos , val = parse_value (src , pos , parse_float , nest_lvl )
491
505
array .append (val )
492
506
pos = skip_comments_and_array_ws (src , pos )
493
507
@@ -503,7 +517,9 @@ def parse_array(src: str, pos: Pos, parse_float: ParseFloat) -> tuple[Pos, list]
503
517
return pos + 1 , array
504
518
505
519
506
- def parse_inline_table (src : str , pos : Pos , parse_float : ParseFloat ) -> tuple [Pos , dict ]:
520
+ def parse_inline_table (
521
+ src : str , pos : Pos , parse_float : ParseFloat , nest_lvl : int
522
+ ) -> tuple [Pos , dict ]:
507
523
pos += 1
508
524
nested_dict = NestedDict ()
509
525
flags = Flags ()
@@ -512,7 +528,7 @@ def parse_inline_table(src: str, pos: Pos, parse_float: ParseFloat) -> tuple[Pos
512
528
if src .startswith ("}" , pos ):
513
529
return pos + 1 , nested_dict .dict
514
530
while True :
515
- pos , key , value = parse_key_value_pair (src , pos , parse_float )
531
+ pos , key , value = parse_key_value_pair (src , pos , parse_float , nest_lvl )
516
532
key_parent , key_stem = key [:- 1 ], key [- 1 ]
517
533
if flags .is_ (key , Flags .FROZEN ):
518
534
raise TOMLDecodeError (f"Cannot mutate immutable namespace { key } " , src , pos )
@@ -654,8 +670,16 @@ def parse_basic_str(src: str, pos: Pos, *, multiline: bool) -> tuple[Pos, str]:
654
670
655
671
656
672
def parse_value ( # noqa: C901
657
- src : str , pos : Pos , parse_float : ParseFloat
673
+ src : str , pos : Pos , parse_float : ParseFloat , nest_lvl : int
658
674
) -> tuple [Pos , Any ]:
675
+ if nest_lvl > MAX_INLINE_NESTING :
676
+ # Pure Python should have raised RecursionError already.
677
+ # This ensures mypyc binaries eventually do the same.
678
+ raise RecursionError ( # pragma: no cover
679
+ "TOML inline arrays/tables are nested more than the allowed"
680
+ f" { MAX_INLINE_NESTING } levels"
681
+ )
682
+
659
683
try :
660
684
char : str | None = src [pos ]
661
685
except IndexError :
@@ -685,11 +709,11 @@ def parse_value( # noqa: C901
685
709
686
710
# Arrays
687
711
if char == "[" :
688
- return parse_array (src , pos , parse_float )
712
+ return parse_array (src , pos , parse_float , nest_lvl + 1 )
689
713
690
714
# Inline tables
691
715
if char == "{" :
692
- return parse_inline_table (src , pos , parse_float )
716
+ return parse_inline_table (src , pos , parse_float , nest_lvl + 1 )
693
717
694
718
# Dates and times
695
719
datetime_match = RE_DATETIME .match (src , pos )
0 commit comments