xref: /aosp_15_r20/prebuilts/build-tools/common/py3-stdlib/tomllib/_parser.py (revision cda5da8d549138a6648c5ee6d7a49cf8f4a657be)
1*cda5da8dSAndroid Build Coastguard Worker# SPDX-License-Identifier: MIT
2*cda5da8dSAndroid Build Coastguard Worker# SPDX-FileCopyrightText: 2021 Taneli Hukkinen
3*cda5da8dSAndroid Build Coastguard Worker# Licensed to PSF under a Contributor Agreement.
4*cda5da8dSAndroid Build Coastguard Worker
5*cda5da8dSAndroid Build Coastguard Workerfrom __future__ import annotations
6*cda5da8dSAndroid Build Coastguard Worker
7*cda5da8dSAndroid Build Coastguard Workerfrom collections.abc import Iterable
8*cda5da8dSAndroid Build Coastguard Workerimport string
9*cda5da8dSAndroid Build Coastguard Workerfrom types import MappingProxyType
10*cda5da8dSAndroid Build Coastguard Workerfrom typing import Any, BinaryIO, NamedTuple
11*cda5da8dSAndroid Build Coastguard Worker
12*cda5da8dSAndroid Build Coastguard Workerfrom ._re import (
13*cda5da8dSAndroid Build Coastguard Worker    RE_DATETIME,
14*cda5da8dSAndroid Build Coastguard Worker    RE_LOCALTIME,
15*cda5da8dSAndroid Build Coastguard Worker    RE_NUMBER,
16*cda5da8dSAndroid Build Coastguard Worker    match_to_datetime,
17*cda5da8dSAndroid Build Coastguard Worker    match_to_localtime,
18*cda5da8dSAndroid Build Coastguard Worker    match_to_number,
19*cda5da8dSAndroid Build Coastguard Worker)
20*cda5da8dSAndroid Build Coastguard Workerfrom ._types import Key, ParseFloat, Pos
21*cda5da8dSAndroid Build Coastguard Worker
22*cda5da8dSAndroid Build Coastguard WorkerASCII_CTRL = frozenset(chr(i) for i in range(32)) | frozenset(chr(127))
23*cda5da8dSAndroid Build Coastguard Worker
24*cda5da8dSAndroid Build Coastguard Worker# Neither of these sets include quotation mark or backslash. They are
25*cda5da8dSAndroid Build Coastguard Worker# currently handled as separate cases in the parser functions.
26*cda5da8dSAndroid Build Coastguard WorkerILLEGAL_BASIC_STR_CHARS = ASCII_CTRL - frozenset("\t")
27*cda5da8dSAndroid Build Coastguard WorkerILLEGAL_MULTILINE_BASIC_STR_CHARS = ASCII_CTRL - frozenset("\t\n")
28*cda5da8dSAndroid Build Coastguard Worker
29*cda5da8dSAndroid Build Coastguard WorkerILLEGAL_LITERAL_STR_CHARS = ILLEGAL_BASIC_STR_CHARS
30*cda5da8dSAndroid Build Coastguard WorkerILLEGAL_MULTILINE_LITERAL_STR_CHARS = ILLEGAL_MULTILINE_BASIC_STR_CHARS
31*cda5da8dSAndroid Build Coastguard Worker
32*cda5da8dSAndroid Build Coastguard WorkerILLEGAL_COMMENT_CHARS = ILLEGAL_BASIC_STR_CHARS
33*cda5da8dSAndroid Build Coastguard Worker
34*cda5da8dSAndroid Build Coastguard WorkerTOML_WS = frozenset(" \t")
35*cda5da8dSAndroid Build Coastguard WorkerTOML_WS_AND_NEWLINE = TOML_WS | frozenset("\n")
36*cda5da8dSAndroid Build Coastguard WorkerBARE_KEY_CHARS = frozenset(string.ascii_letters + string.digits + "-_")
37*cda5da8dSAndroid Build Coastguard WorkerKEY_INITIAL_CHARS = BARE_KEY_CHARS | frozenset("\"'")
38*cda5da8dSAndroid Build Coastguard WorkerHEXDIGIT_CHARS = frozenset(string.hexdigits)
39*cda5da8dSAndroid Build Coastguard Worker
40*cda5da8dSAndroid Build Coastguard WorkerBASIC_STR_ESCAPE_REPLACEMENTS = MappingProxyType(
41*cda5da8dSAndroid Build Coastguard Worker    {
42*cda5da8dSAndroid Build Coastguard Worker        "\\b": "\u0008",  # backspace
43*cda5da8dSAndroid Build Coastguard Worker        "\\t": "\u0009",  # tab
44*cda5da8dSAndroid Build Coastguard Worker        "\\n": "\u000A",  # linefeed
45*cda5da8dSAndroid Build Coastguard Worker        "\\f": "\u000C",  # form feed
46*cda5da8dSAndroid Build Coastguard Worker        "\\r": "\u000D",  # carriage return
47*cda5da8dSAndroid Build Coastguard Worker        '\\"': "\u0022",  # quote
48*cda5da8dSAndroid Build Coastguard Worker        "\\\\": "\u005C",  # backslash
49*cda5da8dSAndroid Build Coastguard Worker    }
50*cda5da8dSAndroid Build Coastguard Worker)
51*cda5da8dSAndroid Build Coastguard Worker
52*cda5da8dSAndroid Build Coastguard Worker
53*cda5da8dSAndroid Build Coastguard Workerclass TOMLDecodeError(ValueError):
54*cda5da8dSAndroid Build Coastguard Worker    """An error raised if a document is not valid TOML."""
55*cda5da8dSAndroid Build Coastguard Worker
56*cda5da8dSAndroid Build Coastguard Worker
57*cda5da8dSAndroid Build Coastguard Workerdef load(fp: BinaryIO, /, *, parse_float: ParseFloat = float) -> dict[str, Any]:
58*cda5da8dSAndroid Build Coastguard Worker    """Parse TOML from a binary file object."""
59*cda5da8dSAndroid Build Coastguard Worker    b = fp.read()
60*cda5da8dSAndroid Build Coastguard Worker    try:
61*cda5da8dSAndroid Build Coastguard Worker        s = b.decode()
62*cda5da8dSAndroid Build Coastguard Worker    except AttributeError:
63*cda5da8dSAndroid Build Coastguard Worker        raise TypeError(
64*cda5da8dSAndroid Build Coastguard Worker            "File must be opened in binary mode, e.g. use `open('foo.toml', 'rb')`"
65*cda5da8dSAndroid Build Coastguard Worker        ) from None
66*cda5da8dSAndroid Build Coastguard Worker    return loads(s, parse_float=parse_float)
67*cda5da8dSAndroid Build Coastguard Worker
68*cda5da8dSAndroid Build Coastguard Worker
69*cda5da8dSAndroid Build Coastguard Workerdef loads(s: str, /, *, parse_float: ParseFloat = float) -> dict[str, Any]:  # noqa: C901
70*cda5da8dSAndroid Build Coastguard Worker    """Parse TOML from a string."""
71*cda5da8dSAndroid Build Coastguard Worker
72*cda5da8dSAndroid Build Coastguard Worker    # The spec allows converting "\r\n" to "\n", even in string
73*cda5da8dSAndroid Build Coastguard Worker    # literals. Let's do so to simplify parsing.
74*cda5da8dSAndroid Build Coastguard Worker    src = s.replace("\r\n", "\n")
75*cda5da8dSAndroid Build Coastguard Worker    pos = 0
76*cda5da8dSAndroid Build Coastguard Worker    out = Output(NestedDict(), Flags())
77*cda5da8dSAndroid Build Coastguard Worker    header: Key = ()
78*cda5da8dSAndroid Build Coastguard Worker    parse_float = make_safe_parse_float(parse_float)
79*cda5da8dSAndroid Build Coastguard Worker
80*cda5da8dSAndroid Build Coastguard Worker    # Parse one statement at a time
81*cda5da8dSAndroid Build Coastguard Worker    # (typically means one line in TOML source)
82*cda5da8dSAndroid Build Coastguard Worker    while True:
83*cda5da8dSAndroid Build Coastguard Worker        # 1. Skip line leading whitespace
84*cda5da8dSAndroid Build Coastguard Worker        pos = skip_chars(src, pos, TOML_WS)
85*cda5da8dSAndroid Build Coastguard Worker
86*cda5da8dSAndroid Build Coastguard Worker        # 2. Parse rules. Expect one of the following:
87*cda5da8dSAndroid Build Coastguard Worker        #    - end of file
88*cda5da8dSAndroid Build Coastguard Worker        #    - end of line
89*cda5da8dSAndroid Build Coastguard Worker        #    - comment
90*cda5da8dSAndroid Build Coastguard Worker        #    - key/value pair
91*cda5da8dSAndroid Build Coastguard Worker        #    - append dict to list (and move to its namespace)
92*cda5da8dSAndroid Build Coastguard Worker        #    - create dict (and move to its namespace)
93*cda5da8dSAndroid Build Coastguard Worker        # Skip trailing whitespace when applicable.
94*cda5da8dSAndroid Build Coastguard Worker        try:
95*cda5da8dSAndroid Build Coastguard Worker            char = src[pos]
96*cda5da8dSAndroid Build Coastguard Worker        except IndexError:
97*cda5da8dSAndroid Build Coastguard Worker            break
98*cda5da8dSAndroid Build Coastguard Worker        if char == "\n":
99*cda5da8dSAndroid Build Coastguard Worker            pos += 1
100*cda5da8dSAndroid Build Coastguard Worker            continue
101*cda5da8dSAndroid Build Coastguard Worker        if char in KEY_INITIAL_CHARS:
102*cda5da8dSAndroid Build Coastguard Worker            pos = key_value_rule(src, pos, out, header, parse_float)
103*cda5da8dSAndroid Build Coastguard Worker            pos = skip_chars(src, pos, TOML_WS)
104*cda5da8dSAndroid Build Coastguard Worker        elif char == "[":
105*cda5da8dSAndroid Build Coastguard Worker            try:
106*cda5da8dSAndroid Build Coastguard Worker                second_char: str | None = src[pos + 1]
107*cda5da8dSAndroid Build Coastguard Worker            except IndexError:
108*cda5da8dSAndroid Build Coastguard Worker                second_char = None
109*cda5da8dSAndroid Build Coastguard Worker            out.flags.finalize_pending()
110*cda5da8dSAndroid Build Coastguard Worker            if second_char == "[":
111*cda5da8dSAndroid Build Coastguard Worker                pos, header = create_list_rule(src, pos, out)
112*cda5da8dSAndroid Build Coastguard Worker            else:
113*cda5da8dSAndroid Build Coastguard Worker                pos, header = create_dict_rule(src, pos, out)
114*cda5da8dSAndroid Build Coastguard Worker            pos = skip_chars(src, pos, TOML_WS)
115*cda5da8dSAndroid Build Coastguard Worker        elif char != "#":
116*cda5da8dSAndroid Build Coastguard Worker            raise suffixed_err(src, pos, "Invalid statement")
117*cda5da8dSAndroid Build Coastguard Worker
118*cda5da8dSAndroid Build Coastguard Worker        # 3. Skip comment
119*cda5da8dSAndroid Build Coastguard Worker        pos = skip_comment(src, pos)
120*cda5da8dSAndroid Build Coastguard Worker
121*cda5da8dSAndroid Build Coastguard Worker        # 4. Expect end of line or end of file
122*cda5da8dSAndroid Build Coastguard Worker        try:
123*cda5da8dSAndroid Build Coastguard Worker            char = src[pos]
124*cda5da8dSAndroid Build Coastguard Worker        except IndexError:
125*cda5da8dSAndroid Build Coastguard Worker            break
126*cda5da8dSAndroid Build Coastguard Worker        if char != "\n":
127*cda5da8dSAndroid Build Coastguard Worker            raise suffixed_err(
128*cda5da8dSAndroid Build Coastguard Worker                src, pos, "Expected newline or end of document after a statement"
129*cda5da8dSAndroid Build Coastguard Worker            )
130*cda5da8dSAndroid Build Coastguard Worker        pos += 1
131*cda5da8dSAndroid Build Coastguard Worker
132*cda5da8dSAndroid Build Coastguard Worker    return out.data.dict
133*cda5da8dSAndroid Build Coastguard Worker
134*cda5da8dSAndroid Build Coastguard Worker
135*cda5da8dSAndroid Build Coastguard Workerclass Flags:
136*cda5da8dSAndroid Build Coastguard Worker    """Flags that map to parsed keys/namespaces."""
137*cda5da8dSAndroid Build Coastguard Worker
138*cda5da8dSAndroid Build Coastguard Worker    # Marks an immutable namespace (inline array or inline table).
139*cda5da8dSAndroid Build Coastguard Worker    FROZEN = 0
140*cda5da8dSAndroid Build Coastguard Worker    # Marks a nest that has been explicitly created and can no longer
141*cda5da8dSAndroid Build Coastguard Worker    # be opened using the "[table]" syntax.
142*cda5da8dSAndroid Build Coastguard Worker    EXPLICIT_NEST = 1
143*cda5da8dSAndroid Build Coastguard Worker
144*cda5da8dSAndroid Build Coastguard Worker    def __init__(self) -> None:
145*cda5da8dSAndroid Build Coastguard Worker        self._flags: dict[str, dict] = {}
146*cda5da8dSAndroid Build Coastguard Worker        self._pending_flags: set[tuple[Key, int]] = set()
147*cda5da8dSAndroid Build Coastguard Worker
148*cda5da8dSAndroid Build Coastguard Worker    def add_pending(self, key: Key, flag: int) -> None:
149*cda5da8dSAndroid Build Coastguard Worker        self._pending_flags.add((key, flag))
150*cda5da8dSAndroid Build Coastguard Worker
151*cda5da8dSAndroid Build Coastguard Worker    def finalize_pending(self) -> None:
152*cda5da8dSAndroid Build Coastguard Worker        for key, flag in self._pending_flags:
153*cda5da8dSAndroid Build Coastguard Worker            self.set(key, flag, recursive=False)
154*cda5da8dSAndroid Build Coastguard Worker        self._pending_flags.clear()
155*cda5da8dSAndroid Build Coastguard Worker
156*cda5da8dSAndroid Build Coastguard Worker    def unset_all(self, key: Key) -> None:
157*cda5da8dSAndroid Build Coastguard Worker        cont = self._flags
158*cda5da8dSAndroid Build Coastguard Worker        for k in key[:-1]:
159*cda5da8dSAndroid Build Coastguard Worker            if k not in cont:
160*cda5da8dSAndroid Build Coastguard Worker                return
161*cda5da8dSAndroid Build Coastguard Worker            cont = cont[k]["nested"]
162*cda5da8dSAndroid Build Coastguard Worker        cont.pop(key[-1], None)
163*cda5da8dSAndroid Build Coastguard Worker
164*cda5da8dSAndroid Build Coastguard Worker    def set(self, key: Key, flag: int, *, recursive: bool) -> None:  # noqa: A003
165*cda5da8dSAndroid Build Coastguard Worker        cont = self._flags
166*cda5da8dSAndroid Build Coastguard Worker        key_parent, key_stem = key[:-1], key[-1]
167*cda5da8dSAndroid Build Coastguard Worker        for k in key_parent:
168*cda5da8dSAndroid Build Coastguard Worker            if k not in cont:
169*cda5da8dSAndroid Build Coastguard Worker                cont[k] = {"flags": set(), "recursive_flags": set(), "nested": {}}
170*cda5da8dSAndroid Build Coastguard Worker            cont = cont[k]["nested"]
171*cda5da8dSAndroid Build Coastguard Worker        if key_stem not in cont:
172*cda5da8dSAndroid Build Coastguard Worker            cont[key_stem] = {"flags": set(), "recursive_flags": set(), "nested": {}}
173*cda5da8dSAndroid Build Coastguard Worker        cont[key_stem]["recursive_flags" if recursive else "flags"].add(flag)
174*cda5da8dSAndroid Build Coastguard Worker
175*cda5da8dSAndroid Build Coastguard Worker    def is_(self, key: Key, flag: int) -> bool:
176*cda5da8dSAndroid Build Coastguard Worker        if not key:
177*cda5da8dSAndroid Build Coastguard Worker            return False  # document root has no flags
178*cda5da8dSAndroid Build Coastguard Worker        cont = self._flags
179*cda5da8dSAndroid Build Coastguard Worker        for k in key[:-1]:
180*cda5da8dSAndroid Build Coastguard Worker            if k not in cont:
181*cda5da8dSAndroid Build Coastguard Worker                return False
182*cda5da8dSAndroid Build Coastguard Worker            inner_cont = cont[k]
183*cda5da8dSAndroid Build Coastguard Worker            if flag in inner_cont["recursive_flags"]:
184*cda5da8dSAndroid Build Coastguard Worker                return True
185*cda5da8dSAndroid Build Coastguard Worker            cont = inner_cont["nested"]
186*cda5da8dSAndroid Build Coastguard Worker        key_stem = key[-1]
187*cda5da8dSAndroid Build Coastguard Worker        if key_stem in cont:
188*cda5da8dSAndroid Build Coastguard Worker            cont = cont[key_stem]
189*cda5da8dSAndroid Build Coastguard Worker            return flag in cont["flags"] or flag in cont["recursive_flags"]
190*cda5da8dSAndroid Build Coastguard Worker        return False
191*cda5da8dSAndroid Build Coastguard Worker
192*cda5da8dSAndroid Build Coastguard Worker
193*cda5da8dSAndroid Build Coastguard Workerclass NestedDict:
194*cda5da8dSAndroid Build Coastguard Worker    def __init__(self) -> None:
195*cda5da8dSAndroid Build Coastguard Worker        # The parsed content of the TOML document
196*cda5da8dSAndroid Build Coastguard Worker        self.dict: dict[str, Any] = {}
197*cda5da8dSAndroid Build Coastguard Worker
198*cda5da8dSAndroid Build Coastguard Worker    def get_or_create_nest(
199*cda5da8dSAndroid Build Coastguard Worker        self,
200*cda5da8dSAndroid Build Coastguard Worker        key: Key,
201*cda5da8dSAndroid Build Coastguard Worker        *,
202*cda5da8dSAndroid Build Coastguard Worker        access_lists: bool = True,
203*cda5da8dSAndroid Build Coastguard Worker    ) -> dict:
204*cda5da8dSAndroid Build Coastguard Worker        cont: Any = self.dict
205*cda5da8dSAndroid Build Coastguard Worker        for k in key:
206*cda5da8dSAndroid Build Coastguard Worker            if k not in cont:
207*cda5da8dSAndroid Build Coastguard Worker                cont[k] = {}
208*cda5da8dSAndroid Build Coastguard Worker            cont = cont[k]
209*cda5da8dSAndroid Build Coastguard Worker            if access_lists and isinstance(cont, list):
210*cda5da8dSAndroid Build Coastguard Worker                cont = cont[-1]
211*cda5da8dSAndroid Build Coastguard Worker            if not isinstance(cont, dict):
212*cda5da8dSAndroid Build Coastguard Worker                raise KeyError("There is no nest behind this key")
213*cda5da8dSAndroid Build Coastguard Worker        return cont
214*cda5da8dSAndroid Build Coastguard Worker
215*cda5da8dSAndroid Build Coastguard Worker    def append_nest_to_list(self, key: Key) -> None:
216*cda5da8dSAndroid Build Coastguard Worker        cont = self.get_or_create_nest(key[:-1])
217*cda5da8dSAndroid Build Coastguard Worker        last_key = key[-1]
218*cda5da8dSAndroid Build Coastguard Worker        if last_key in cont:
219*cda5da8dSAndroid Build Coastguard Worker            list_ = cont[last_key]
220*cda5da8dSAndroid Build Coastguard Worker            if not isinstance(list_, list):
221*cda5da8dSAndroid Build Coastguard Worker                raise KeyError("An object other than list found behind this key")
222*cda5da8dSAndroid Build Coastguard Worker            list_.append({})
223*cda5da8dSAndroid Build Coastguard Worker        else:
224*cda5da8dSAndroid Build Coastguard Worker            cont[last_key] = [{}]
225*cda5da8dSAndroid Build Coastguard Worker
226*cda5da8dSAndroid Build Coastguard Worker
227*cda5da8dSAndroid Build Coastguard Workerclass Output(NamedTuple):
228*cda5da8dSAndroid Build Coastguard Worker    data: NestedDict
229*cda5da8dSAndroid Build Coastguard Worker    flags: Flags
230*cda5da8dSAndroid Build Coastguard Worker
231*cda5da8dSAndroid Build Coastguard Worker
232*cda5da8dSAndroid Build Coastguard Workerdef skip_chars(src: str, pos: Pos, chars: Iterable[str]) -> Pos:
233*cda5da8dSAndroid Build Coastguard Worker    try:
234*cda5da8dSAndroid Build Coastguard Worker        while src[pos] in chars:
235*cda5da8dSAndroid Build Coastguard Worker            pos += 1
236*cda5da8dSAndroid Build Coastguard Worker    except IndexError:
237*cda5da8dSAndroid Build Coastguard Worker        pass
238*cda5da8dSAndroid Build Coastguard Worker    return pos
239*cda5da8dSAndroid Build Coastguard Worker
240*cda5da8dSAndroid Build Coastguard Worker
241*cda5da8dSAndroid Build Coastguard Workerdef skip_until(
242*cda5da8dSAndroid Build Coastguard Worker    src: str,
243*cda5da8dSAndroid Build Coastguard Worker    pos: Pos,
244*cda5da8dSAndroid Build Coastguard Worker    expect: str,
245*cda5da8dSAndroid Build Coastguard Worker    *,
246*cda5da8dSAndroid Build Coastguard Worker    error_on: frozenset[str],
247*cda5da8dSAndroid Build Coastguard Worker    error_on_eof: bool,
248*cda5da8dSAndroid Build Coastguard Worker) -> Pos:
249*cda5da8dSAndroid Build Coastguard Worker    try:
250*cda5da8dSAndroid Build Coastguard Worker        new_pos = src.index(expect, pos)
251*cda5da8dSAndroid Build Coastguard Worker    except ValueError:
252*cda5da8dSAndroid Build Coastguard Worker        new_pos = len(src)
253*cda5da8dSAndroid Build Coastguard Worker        if error_on_eof:
254*cda5da8dSAndroid Build Coastguard Worker            raise suffixed_err(src, new_pos, f"Expected {expect!r}") from None
255*cda5da8dSAndroid Build Coastguard Worker
256*cda5da8dSAndroid Build Coastguard Worker    if not error_on.isdisjoint(src[pos:new_pos]):
257*cda5da8dSAndroid Build Coastguard Worker        while src[pos] not in error_on:
258*cda5da8dSAndroid Build Coastguard Worker            pos += 1
259*cda5da8dSAndroid Build Coastguard Worker        raise suffixed_err(src, pos, f"Found invalid character {src[pos]!r}")
260*cda5da8dSAndroid Build Coastguard Worker    return new_pos
261*cda5da8dSAndroid Build Coastguard Worker
262*cda5da8dSAndroid Build Coastguard Worker
263*cda5da8dSAndroid Build Coastguard Workerdef skip_comment(src: str, pos: Pos) -> Pos:
264*cda5da8dSAndroid Build Coastguard Worker    try:
265*cda5da8dSAndroid Build Coastguard Worker        char: str | None = src[pos]
266*cda5da8dSAndroid Build Coastguard Worker    except IndexError:
267*cda5da8dSAndroid Build Coastguard Worker        char = None
268*cda5da8dSAndroid Build Coastguard Worker    if char == "#":
269*cda5da8dSAndroid Build Coastguard Worker        return skip_until(
270*cda5da8dSAndroid Build Coastguard Worker            src, pos + 1, "\n", error_on=ILLEGAL_COMMENT_CHARS, error_on_eof=False
271*cda5da8dSAndroid Build Coastguard Worker        )
272*cda5da8dSAndroid Build Coastguard Worker    return pos
273*cda5da8dSAndroid Build Coastguard Worker
274*cda5da8dSAndroid Build Coastguard Worker
275*cda5da8dSAndroid Build Coastguard Workerdef skip_comments_and_array_ws(src: str, pos: Pos) -> Pos:
276*cda5da8dSAndroid Build Coastguard Worker    while True:
277*cda5da8dSAndroid Build Coastguard Worker        pos_before_skip = pos
278*cda5da8dSAndroid Build Coastguard Worker        pos = skip_chars(src, pos, TOML_WS_AND_NEWLINE)
279*cda5da8dSAndroid Build Coastguard Worker        pos = skip_comment(src, pos)
280*cda5da8dSAndroid Build Coastguard Worker        if pos == pos_before_skip:
281*cda5da8dSAndroid Build Coastguard Worker            return pos
282*cda5da8dSAndroid Build Coastguard Worker
283*cda5da8dSAndroid Build Coastguard Worker
284*cda5da8dSAndroid Build Coastguard Workerdef create_dict_rule(src: str, pos: Pos, out: Output) -> tuple[Pos, Key]:
285*cda5da8dSAndroid Build Coastguard Worker    pos += 1  # Skip "["
286*cda5da8dSAndroid Build Coastguard Worker    pos = skip_chars(src, pos, TOML_WS)
287*cda5da8dSAndroid Build Coastguard Worker    pos, key = parse_key(src, pos)
288*cda5da8dSAndroid Build Coastguard Worker
289*cda5da8dSAndroid Build Coastguard Worker    if out.flags.is_(key, Flags.EXPLICIT_NEST) or out.flags.is_(key, Flags.FROZEN):
290*cda5da8dSAndroid Build Coastguard Worker        raise suffixed_err(src, pos, f"Cannot declare {key} twice")
291*cda5da8dSAndroid Build Coastguard Worker    out.flags.set(key, Flags.EXPLICIT_NEST, recursive=False)
292*cda5da8dSAndroid Build Coastguard Worker    try:
293*cda5da8dSAndroid Build Coastguard Worker        out.data.get_or_create_nest(key)
294*cda5da8dSAndroid Build Coastguard Worker    except KeyError:
295*cda5da8dSAndroid Build Coastguard Worker        raise suffixed_err(src, pos, "Cannot overwrite a value") from None
296*cda5da8dSAndroid Build Coastguard Worker
297*cda5da8dSAndroid Build Coastguard Worker    if not src.startswith("]", pos):
298*cda5da8dSAndroid Build Coastguard Worker        raise suffixed_err(src, pos, "Expected ']' at the end of a table declaration")
299*cda5da8dSAndroid Build Coastguard Worker    return pos + 1, key
300*cda5da8dSAndroid Build Coastguard Worker
301*cda5da8dSAndroid Build Coastguard Worker
302*cda5da8dSAndroid Build Coastguard Workerdef create_list_rule(src: str, pos: Pos, out: Output) -> tuple[Pos, Key]:
303*cda5da8dSAndroid Build Coastguard Worker    pos += 2  # Skip "[["
304*cda5da8dSAndroid Build Coastguard Worker    pos = skip_chars(src, pos, TOML_WS)
305*cda5da8dSAndroid Build Coastguard Worker    pos, key = parse_key(src, pos)
306*cda5da8dSAndroid Build Coastguard Worker
307*cda5da8dSAndroid Build Coastguard Worker    if out.flags.is_(key, Flags.FROZEN):
308*cda5da8dSAndroid Build Coastguard Worker        raise suffixed_err(src, pos, f"Cannot mutate immutable namespace {key}")
309*cda5da8dSAndroid Build Coastguard Worker    # Free the namespace now that it points to another empty list item...
310*cda5da8dSAndroid Build Coastguard Worker    out.flags.unset_all(key)
311*cda5da8dSAndroid Build Coastguard Worker    # ...but this key precisely is still prohibited from table declaration
312*cda5da8dSAndroid Build Coastguard Worker    out.flags.set(key, Flags.EXPLICIT_NEST, recursive=False)
313*cda5da8dSAndroid Build Coastguard Worker    try:
314*cda5da8dSAndroid Build Coastguard Worker        out.data.append_nest_to_list(key)
315*cda5da8dSAndroid Build Coastguard Worker    except KeyError:
316*cda5da8dSAndroid Build Coastguard Worker        raise suffixed_err(src, pos, "Cannot overwrite a value") from None
317*cda5da8dSAndroid Build Coastguard Worker
318*cda5da8dSAndroid Build Coastguard Worker    if not src.startswith("]]", pos):
319*cda5da8dSAndroid Build Coastguard Worker        raise suffixed_err(src, pos, "Expected ']]' at the end of an array declaration")
320*cda5da8dSAndroid Build Coastguard Worker    return pos + 2, key
321*cda5da8dSAndroid Build Coastguard Worker
322*cda5da8dSAndroid Build Coastguard Worker
323*cda5da8dSAndroid Build Coastguard Workerdef key_value_rule(
324*cda5da8dSAndroid Build Coastguard Worker    src: str, pos: Pos, out: Output, header: Key, parse_float: ParseFloat
325*cda5da8dSAndroid Build Coastguard Worker) -> Pos:
326*cda5da8dSAndroid Build Coastguard Worker    pos, key, value = parse_key_value_pair(src, pos, parse_float)
327*cda5da8dSAndroid Build Coastguard Worker    key_parent, key_stem = key[:-1], key[-1]
328*cda5da8dSAndroid Build Coastguard Worker    abs_key_parent = header + key_parent
329*cda5da8dSAndroid Build Coastguard Worker
330*cda5da8dSAndroid Build Coastguard Worker    relative_path_cont_keys = (header + key[:i] for i in range(1, len(key)))
331*cda5da8dSAndroid Build Coastguard Worker    for cont_key in relative_path_cont_keys:
332*cda5da8dSAndroid Build Coastguard Worker        # Check that dotted key syntax does not redefine an existing table
333*cda5da8dSAndroid Build Coastguard Worker        if out.flags.is_(cont_key, Flags.EXPLICIT_NEST):
334*cda5da8dSAndroid Build Coastguard Worker            raise suffixed_err(src, pos, f"Cannot redefine namespace {cont_key}")
335*cda5da8dSAndroid Build Coastguard Worker        # Containers in the relative path can't be opened with the table syntax or
336*cda5da8dSAndroid Build Coastguard Worker        # dotted key/value syntax in following table sections.
337*cda5da8dSAndroid Build Coastguard Worker        out.flags.add_pending(cont_key, Flags.EXPLICIT_NEST)
338*cda5da8dSAndroid Build Coastguard Worker
339*cda5da8dSAndroid Build Coastguard Worker    if out.flags.is_(abs_key_parent, Flags.FROZEN):
340*cda5da8dSAndroid Build Coastguard Worker        raise suffixed_err(
341*cda5da8dSAndroid Build Coastguard Worker            src, pos, f"Cannot mutate immutable namespace {abs_key_parent}"
342*cda5da8dSAndroid Build Coastguard Worker        )
343*cda5da8dSAndroid Build Coastguard Worker
344*cda5da8dSAndroid Build Coastguard Worker    try:
345*cda5da8dSAndroid Build Coastguard Worker        nest = out.data.get_or_create_nest(abs_key_parent)
346*cda5da8dSAndroid Build Coastguard Worker    except KeyError:
347*cda5da8dSAndroid Build Coastguard Worker        raise suffixed_err(src, pos, "Cannot overwrite a value") from None
348*cda5da8dSAndroid Build Coastguard Worker    if key_stem in nest:
349*cda5da8dSAndroid Build Coastguard Worker        raise suffixed_err(src, pos, "Cannot overwrite a value")
350*cda5da8dSAndroid Build Coastguard Worker    # Mark inline table and array namespaces recursively immutable
351*cda5da8dSAndroid Build Coastguard Worker    if isinstance(value, (dict, list)):
352*cda5da8dSAndroid Build Coastguard Worker        out.flags.set(header + key, Flags.FROZEN, recursive=True)
353*cda5da8dSAndroid Build Coastguard Worker    nest[key_stem] = value
354*cda5da8dSAndroid Build Coastguard Worker    return pos
355*cda5da8dSAndroid Build Coastguard Worker
356*cda5da8dSAndroid Build Coastguard Worker
357*cda5da8dSAndroid Build Coastguard Workerdef parse_key_value_pair(
358*cda5da8dSAndroid Build Coastguard Worker    src: str, pos: Pos, parse_float: ParseFloat
359*cda5da8dSAndroid Build Coastguard Worker) -> tuple[Pos, Key, Any]:
360*cda5da8dSAndroid Build Coastguard Worker    pos, key = parse_key(src, pos)
361*cda5da8dSAndroid Build Coastguard Worker    try:
362*cda5da8dSAndroid Build Coastguard Worker        char: str | None = src[pos]
363*cda5da8dSAndroid Build Coastguard Worker    except IndexError:
364*cda5da8dSAndroid Build Coastguard Worker        char = None
365*cda5da8dSAndroid Build Coastguard Worker    if char != "=":
366*cda5da8dSAndroid Build Coastguard Worker        raise suffixed_err(src, pos, "Expected '=' after a key in a key/value pair")
367*cda5da8dSAndroid Build Coastguard Worker    pos += 1
368*cda5da8dSAndroid Build Coastguard Worker    pos = skip_chars(src, pos, TOML_WS)
369*cda5da8dSAndroid Build Coastguard Worker    pos, value = parse_value(src, pos, parse_float)
370*cda5da8dSAndroid Build Coastguard Worker    return pos, key, value
371*cda5da8dSAndroid Build Coastguard Worker
372*cda5da8dSAndroid Build Coastguard Worker
373*cda5da8dSAndroid Build Coastguard Workerdef parse_key(src: str, pos: Pos) -> tuple[Pos, Key]:
374*cda5da8dSAndroid Build Coastguard Worker    pos, key_part = parse_key_part(src, pos)
375*cda5da8dSAndroid Build Coastguard Worker    key: Key = (key_part,)
376*cda5da8dSAndroid Build Coastguard Worker    pos = skip_chars(src, pos, TOML_WS)
377*cda5da8dSAndroid Build Coastguard Worker    while True:
378*cda5da8dSAndroid Build Coastguard Worker        try:
379*cda5da8dSAndroid Build Coastguard Worker            char: str | None = src[pos]
380*cda5da8dSAndroid Build Coastguard Worker        except IndexError:
381*cda5da8dSAndroid Build Coastguard Worker            char = None
382*cda5da8dSAndroid Build Coastguard Worker        if char != ".":
383*cda5da8dSAndroid Build Coastguard Worker            return pos, key
384*cda5da8dSAndroid Build Coastguard Worker        pos += 1
385*cda5da8dSAndroid Build Coastguard Worker        pos = skip_chars(src, pos, TOML_WS)
386*cda5da8dSAndroid Build Coastguard Worker        pos, key_part = parse_key_part(src, pos)
387*cda5da8dSAndroid Build Coastguard Worker        key += (key_part,)
388*cda5da8dSAndroid Build Coastguard Worker        pos = skip_chars(src, pos, TOML_WS)
389*cda5da8dSAndroid Build Coastguard Worker
390*cda5da8dSAndroid Build Coastguard Worker
391*cda5da8dSAndroid Build Coastguard Workerdef parse_key_part(src: str, pos: Pos) -> tuple[Pos, str]:
392*cda5da8dSAndroid Build Coastguard Worker    try:
393*cda5da8dSAndroid Build Coastguard Worker        char: str | None = src[pos]
394*cda5da8dSAndroid Build Coastguard Worker    except IndexError:
395*cda5da8dSAndroid Build Coastguard Worker        char = None
396*cda5da8dSAndroid Build Coastguard Worker    if char in BARE_KEY_CHARS:
397*cda5da8dSAndroid Build Coastguard Worker        start_pos = pos
398*cda5da8dSAndroid Build Coastguard Worker        pos = skip_chars(src, pos, BARE_KEY_CHARS)
399*cda5da8dSAndroid Build Coastguard Worker        return pos, src[start_pos:pos]
400*cda5da8dSAndroid Build Coastguard Worker    if char == "'":
401*cda5da8dSAndroid Build Coastguard Worker        return parse_literal_str(src, pos)
402*cda5da8dSAndroid Build Coastguard Worker    if char == '"':
403*cda5da8dSAndroid Build Coastguard Worker        return parse_one_line_basic_str(src, pos)
404*cda5da8dSAndroid Build Coastguard Worker    raise suffixed_err(src, pos, "Invalid initial character for a key part")
405*cda5da8dSAndroid Build Coastguard Worker
406*cda5da8dSAndroid Build Coastguard Worker
407*cda5da8dSAndroid Build Coastguard Workerdef parse_one_line_basic_str(src: str, pos: Pos) -> tuple[Pos, str]:
408*cda5da8dSAndroid Build Coastguard Worker    pos += 1
409*cda5da8dSAndroid Build Coastguard Worker    return parse_basic_str(src, pos, multiline=False)
410*cda5da8dSAndroid Build Coastguard Worker
411*cda5da8dSAndroid Build Coastguard Worker
412*cda5da8dSAndroid Build Coastguard Workerdef parse_array(src: str, pos: Pos, parse_float: ParseFloat) -> tuple[Pos, list]:
413*cda5da8dSAndroid Build Coastguard Worker    pos += 1
414*cda5da8dSAndroid Build Coastguard Worker    array: list = []
415*cda5da8dSAndroid Build Coastguard Worker
416*cda5da8dSAndroid Build Coastguard Worker    pos = skip_comments_and_array_ws(src, pos)
417*cda5da8dSAndroid Build Coastguard Worker    if src.startswith("]", pos):
418*cda5da8dSAndroid Build Coastguard Worker        return pos + 1, array
419*cda5da8dSAndroid Build Coastguard Worker    while True:
420*cda5da8dSAndroid Build Coastguard Worker        pos, val = parse_value(src, pos, parse_float)
421*cda5da8dSAndroid Build Coastguard Worker        array.append(val)
422*cda5da8dSAndroid Build Coastguard Worker        pos = skip_comments_and_array_ws(src, pos)
423*cda5da8dSAndroid Build Coastguard Worker
424*cda5da8dSAndroid Build Coastguard Worker        c = src[pos : pos + 1]
425*cda5da8dSAndroid Build Coastguard Worker        if c == "]":
426*cda5da8dSAndroid Build Coastguard Worker            return pos + 1, array
427*cda5da8dSAndroid Build Coastguard Worker        if c != ",":
428*cda5da8dSAndroid Build Coastguard Worker            raise suffixed_err(src, pos, "Unclosed array")
429*cda5da8dSAndroid Build Coastguard Worker        pos += 1
430*cda5da8dSAndroid Build Coastguard Worker
431*cda5da8dSAndroid Build Coastguard Worker        pos = skip_comments_and_array_ws(src, pos)
432*cda5da8dSAndroid Build Coastguard Worker        if src.startswith("]", pos):
433*cda5da8dSAndroid Build Coastguard Worker            return pos + 1, array
434*cda5da8dSAndroid Build Coastguard Worker
435*cda5da8dSAndroid Build Coastguard Worker
436*cda5da8dSAndroid Build Coastguard Workerdef parse_inline_table(src: str, pos: Pos, parse_float: ParseFloat) -> tuple[Pos, dict]:
437*cda5da8dSAndroid Build Coastguard Worker    pos += 1
438*cda5da8dSAndroid Build Coastguard Worker    nested_dict = NestedDict()
439*cda5da8dSAndroid Build Coastguard Worker    flags = Flags()
440*cda5da8dSAndroid Build Coastguard Worker
441*cda5da8dSAndroid Build Coastguard Worker    pos = skip_chars(src, pos, TOML_WS)
442*cda5da8dSAndroid Build Coastguard Worker    if src.startswith("}", pos):
443*cda5da8dSAndroid Build Coastguard Worker        return pos + 1, nested_dict.dict
444*cda5da8dSAndroid Build Coastguard Worker    while True:
445*cda5da8dSAndroid Build Coastguard Worker        pos, key, value = parse_key_value_pair(src, pos, parse_float)
446*cda5da8dSAndroid Build Coastguard Worker        key_parent, key_stem = key[:-1], key[-1]
447*cda5da8dSAndroid Build Coastguard Worker        if flags.is_(key, Flags.FROZEN):
448*cda5da8dSAndroid Build Coastguard Worker            raise suffixed_err(src, pos, f"Cannot mutate immutable namespace {key}")
449*cda5da8dSAndroid Build Coastguard Worker        try:
450*cda5da8dSAndroid Build Coastguard Worker            nest = nested_dict.get_or_create_nest(key_parent, access_lists=False)
451*cda5da8dSAndroid Build Coastguard Worker        except KeyError:
452*cda5da8dSAndroid Build Coastguard Worker            raise suffixed_err(src, pos, "Cannot overwrite a value") from None
453*cda5da8dSAndroid Build Coastguard Worker        if key_stem in nest:
454*cda5da8dSAndroid Build Coastguard Worker            raise suffixed_err(src, pos, f"Duplicate inline table key {key_stem!r}")
455*cda5da8dSAndroid Build Coastguard Worker        nest[key_stem] = value
456*cda5da8dSAndroid Build Coastguard Worker        pos = skip_chars(src, pos, TOML_WS)
457*cda5da8dSAndroid Build Coastguard Worker        c = src[pos : pos + 1]
458*cda5da8dSAndroid Build Coastguard Worker        if c == "}":
459*cda5da8dSAndroid Build Coastguard Worker            return pos + 1, nested_dict.dict
460*cda5da8dSAndroid Build Coastguard Worker        if c != ",":
461*cda5da8dSAndroid Build Coastguard Worker            raise suffixed_err(src, pos, "Unclosed inline table")
462*cda5da8dSAndroid Build Coastguard Worker        if isinstance(value, (dict, list)):
463*cda5da8dSAndroid Build Coastguard Worker            flags.set(key, Flags.FROZEN, recursive=True)
464*cda5da8dSAndroid Build Coastguard Worker        pos += 1
465*cda5da8dSAndroid Build Coastguard Worker        pos = skip_chars(src, pos, TOML_WS)
466*cda5da8dSAndroid Build Coastguard Worker
467*cda5da8dSAndroid Build Coastguard Worker
468*cda5da8dSAndroid Build Coastguard Workerdef parse_basic_str_escape(
469*cda5da8dSAndroid Build Coastguard Worker    src: str, pos: Pos, *, multiline: bool = False
470*cda5da8dSAndroid Build Coastguard Worker) -> tuple[Pos, str]:
471*cda5da8dSAndroid Build Coastguard Worker    escape_id = src[pos : pos + 2]
472*cda5da8dSAndroid Build Coastguard Worker    pos += 2
473*cda5da8dSAndroid Build Coastguard Worker    if multiline and escape_id in {"\\ ", "\\\t", "\\\n"}:
474*cda5da8dSAndroid Build Coastguard Worker        # Skip whitespace until next non-whitespace character or end of
475*cda5da8dSAndroid Build Coastguard Worker        # the doc. Error if non-whitespace is found before newline.
476*cda5da8dSAndroid Build Coastguard Worker        if escape_id != "\\\n":
477*cda5da8dSAndroid Build Coastguard Worker            pos = skip_chars(src, pos, TOML_WS)
478*cda5da8dSAndroid Build Coastguard Worker            try:
479*cda5da8dSAndroid Build Coastguard Worker                char = src[pos]
480*cda5da8dSAndroid Build Coastguard Worker            except IndexError:
481*cda5da8dSAndroid Build Coastguard Worker                return pos, ""
482*cda5da8dSAndroid Build Coastguard Worker            if char != "\n":
483*cda5da8dSAndroid Build Coastguard Worker                raise suffixed_err(src, pos, "Unescaped '\\' in a string")
484*cda5da8dSAndroid Build Coastguard Worker            pos += 1
485*cda5da8dSAndroid Build Coastguard Worker        pos = skip_chars(src, pos, TOML_WS_AND_NEWLINE)
486*cda5da8dSAndroid Build Coastguard Worker        return pos, ""
487*cda5da8dSAndroid Build Coastguard Worker    if escape_id == "\\u":
488*cda5da8dSAndroid Build Coastguard Worker        return parse_hex_char(src, pos, 4)
489*cda5da8dSAndroid Build Coastguard Worker    if escape_id == "\\U":
490*cda5da8dSAndroid Build Coastguard Worker        return parse_hex_char(src, pos, 8)
491*cda5da8dSAndroid Build Coastguard Worker    try:
492*cda5da8dSAndroid Build Coastguard Worker        return pos, BASIC_STR_ESCAPE_REPLACEMENTS[escape_id]
493*cda5da8dSAndroid Build Coastguard Worker    except KeyError:
494*cda5da8dSAndroid Build Coastguard Worker        raise suffixed_err(src, pos, "Unescaped '\\' in a string") from None
495*cda5da8dSAndroid Build Coastguard Worker
496*cda5da8dSAndroid Build Coastguard Worker
497*cda5da8dSAndroid Build Coastguard Workerdef parse_basic_str_escape_multiline(src: str, pos: Pos) -> tuple[Pos, str]:
498*cda5da8dSAndroid Build Coastguard Worker    return parse_basic_str_escape(src, pos, multiline=True)
499*cda5da8dSAndroid Build Coastguard Worker
500*cda5da8dSAndroid Build Coastguard Worker
501*cda5da8dSAndroid Build Coastguard Workerdef parse_hex_char(src: str, pos: Pos, hex_len: int) -> tuple[Pos, str]:
502*cda5da8dSAndroid Build Coastguard Worker    hex_str = src[pos : pos + hex_len]
503*cda5da8dSAndroid Build Coastguard Worker    if len(hex_str) != hex_len or not HEXDIGIT_CHARS.issuperset(hex_str):
504*cda5da8dSAndroid Build Coastguard Worker        raise suffixed_err(src, pos, "Invalid hex value")
505*cda5da8dSAndroid Build Coastguard Worker    pos += hex_len
506*cda5da8dSAndroid Build Coastguard Worker    hex_int = int(hex_str, 16)
507*cda5da8dSAndroid Build Coastguard Worker    if not is_unicode_scalar_value(hex_int):
508*cda5da8dSAndroid Build Coastguard Worker        raise suffixed_err(src, pos, "Escaped character is not a Unicode scalar value")
509*cda5da8dSAndroid Build Coastguard Worker    return pos, chr(hex_int)
510*cda5da8dSAndroid Build Coastguard Worker
511*cda5da8dSAndroid Build Coastguard Worker
512*cda5da8dSAndroid Build Coastguard Workerdef parse_literal_str(src: str, pos: Pos) -> tuple[Pos, str]:
513*cda5da8dSAndroid Build Coastguard Worker    pos += 1  # Skip starting apostrophe
514*cda5da8dSAndroid Build Coastguard Worker    start_pos = pos
515*cda5da8dSAndroid Build Coastguard Worker    pos = skip_until(
516*cda5da8dSAndroid Build Coastguard Worker        src, pos, "'", error_on=ILLEGAL_LITERAL_STR_CHARS, error_on_eof=True
517*cda5da8dSAndroid Build Coastguard Worker    )
518*cda5da8dSAndroid Build Coastguard Worker    return pos + 1, src[start_pos:pos]  # Skip ending apostrophe
519*cda5da8dSAndroid Build Coastguard Worker
520*cda5da8dSAndroid Build Coastguard Worker
521*cda5da8dSAndroid Build Coastguard Workerdef parse_multiline_str(src: str, pos: Pos, *, literal: bool) -> tuple[Pos, str]:
522*cda5da8dSAndroid Build Coastguard Worker    pos += 3
523*cda5da8dSAndroid Build Coastguard Worker    if src.startswith("\n", pos):
524*cda5da8dSAndroid Build Coastguard Worker        pos += 1
525*cda5da8dSAndroid Build Coastguard Worker
526*cda5da8dSAndroid Build Coastguard Worker    if literal:
527*cda5da8dSAndroid Build Coastguard Worker        delim = "'"
528*cda5da8dSAndroid Build Coastguard Worker        end_pos = skip_until(
529*cda5da8dSAndroid Build Coastguard Worker            src,
530*cda5da8dSAndroid Build Coastguard Worker            pos,
531*cda5da8dSAndroid Build Coastguard Worker            "'''",
532*cda5da8dSAndroid Build Coastguard Worker            error_on=ILLEGAL_MULTILINE_LITERAL_STR_CHARS,
533*cda5da8dSAndroid Build Coastguard Worker            error_on_eof=True,
534*cda5da8dSAndroid Build Coastguard Worker        )
535*cda5da8dSAndroid Build Coastguard Worker        result = src[pos:end_pos]
536*cda5da8dSAndroid Build Coastguard Worker        pos = end_pos + 3
537*cda5da8dSAndroid Build Coastguard Worker    else:
538*cda5da8dSAndroid Build Coastguard Worker        delim = '"'
539*cda5da8dSAndroid Build Coastguard Worker        pos, result = parse_basic_str(src, pos, multiline=True)
540*cda5da8dSAndroid Build Coastguard Worker
541*cda5da8dSAndroid Build Coastguard Worker    # Add at maximum two extra apostrophes/quotes if the end sequence
542*cda5da8dSAndroid Build Coastguard Worker    # is 4 or 5 chars long instead of just 3.
543*cda5da8dSAndroid Build Coastguard Worker    if not src.startswith(delim, pos):
544*cda5da8dSAndroid Build Coastguard Worker        return pos, result
545*cda5da8dSAndroid Build Coastguard Worker    pos += 1
546*cda5da8dSAndroid Build Coastguard Worker    if not src.startswith(delim, pos):
547*cda5da8dSAndroid Build Coastguard Worker        return pos, result + delim
548*cda5da8dSAndroid Build Coastguard Worker    pos += 1
549*cda5da8dSAndroid Build Coastguard Worker    return pos, result + (delim * 2)
550*cda5da8dSAndroid Build Coastguard Worker
551*cda5da8dSAndroid Build Coastguard Worker
552*cda5da8dSAndroid Build Coastguard Workerdef parse_basic_str(src: str, pos: Pos, *, multiline: bool) -> tuple[Pos, str]:
553*cda5da8dSAndroid Build Coastguard Worker    if multiline:
554*cda5da8dSAndroid Build Coastguard Worker        error_on = ILLEGAL_MULTILINE_BASIC_STR_CHARS
555*cda5da8dSAndroid Build Coastguard Worker        parse_escapes = parse_basic_str_escape_multiline
556*cda5da8dSAndroid Build Coastguard Worker    else:
557*cda5da8dSAndroid Build Coastguard Worker        error_on = ILLEGAL_BASIC_STR_CHARS
558*cda5da8dSAndroid Build Coastguard Worker        parse_escapes = parse_basic_str_escape
559*cda5da8dSAndroid Build Coastguard Worker    result = ""
560*cda5da8dSAndroid Build Coastguard Worker    start_pos = pos
561*cda5da8dSAndroid Build Coastguard Worker    while True:
562*cda5da8dSAndroid Build Coastguard Worker        try:
563*cda5da8dSAndroid Build Coastguard Worker            char = src[pos]
564*cda5da8dSAndroid Build Coastguard Worker        except IndexError:
565*cda5da8dSAndroid Build Coastguard Worker            raise suffixed_err(src, pos, "Unterminated string") from None
566*cda5da8dSAndroid Build Coastguard Worker        if char == '"':
567*cda5da8dSAndroid Build Coastguard Worker            if not multiline:
568*cda5da8dSAndroid Build Coastguard Worker                return pos + 1, result + src[start_pos:pos]
569*cda5da8dSAndroid Build Coastguard Worker            if src.startswith('"""', pos):
570*cda5da8dSAndroid Build Coastguard Worker                return pos + 3, result + src[start_pos:pos]
571*cda5da8dSAndroid Build Coastguard Worker            pos += 1
572*cda5da8dSAndroid Build Coastguard Worker            continue
573*cda5da8dSAndroid Build Coastguard Worker        if char == "\\":
574*cda5da8dSAndroid Build Coastguard Worker            result += src[start_pos:pos]
575*cda5da8dSAndroid Build Coastguard Worker            pos, parsed_escape = parse_escapes(src, pos)
576*cda5da8dSAndroid Build Coastguard Worker            result += parsed_escape
577*cda5da8dSAndroid Build Coastguard Worker            start_pos = pos
578*cda5da8dSAndroid Build Coastguard Worker            continue
579*cda5da8dSAndroid Build Coastguard Worker        if char in error_on:
580*cda5da8dSAndroid Build Coastguard Worker            raise suffixed_err(src, pos, f"Illegal character {char!r}")
581*cda5da8dSAndroid Build Coastguard Worker        pos += 1
582*cda5da8dSAndroid Build Coastguard Worker
583*cda5da8dSAndroid Build Coastguard Worker
584*cda5da8dSAndroid Build Coastguard Workerdef parse_value(  # noqa: C901
585*cda5da8dSAndroid Build Coastguard Worker    src: str, pos: Pos, parse_float: ParseFloat
586*cda5da8dSAndroid Build Coastguard Worker) -> tuple[Pos, Any]:
587*cda5da8dSAndroid Build Coastguard Worker    try:
588*cda5da8dSAndroid Build Coastguard Worker        char: str | None = src[pos]
589*cda5da8dSAndroid Build Coastguard Worker    except IndexError:
590*cda5da8dSAndroid Build Coastguard Worker        char = None
591*cda5da8dSAndroid Build Coastguard Worker
592*cda5da8dSAndroid Build Coastguard Worker    # IMPORTANT: order conditions based on speed of checking and likelihood
593*cda5da8dSAndroid Build Coastguard Worker
594*cda5da8dSAndroid Build Coastguard Worker    # Basic strings
595*cda5da8dSAndroid Build Coastguard Worker    if char == '"':
596*cda5da8dSAndroid Build Coastguard Worker        if src.startswith('"""', pos):
597*cda5da8dSAndroid Build Coastguard Worker            return parse_multiline_str(src, pos, literal=False)
598*cda5da8dSAndroid Build Coastguard Worker        return parse_one_line_basic_str(src, pos)
599*cda5da8dSAndroid Build Coastguard Worker
600*cda5da8dSAndroid Build Coastguard Worker    # Literal strings
601*cda5da8dSAndroid Build Coastguard Worker    if char == "'":
602*cda5da8dSAndroid Build Coastguard Worker        if src.startswith("'''", pos):
603*cda5da8dSAndroid Build Coastguard Worker            return parse_multiline_str(src, pos, literal=True)
604*cda5da8dSAndroid Build Coastguard Worker        return parse_literal_str(src, pos)
605*cda5da8dSAndroid Build Coastguard Worker
606*cda5da8dSAndroid Build Coastguard Worker    # Booleans
607*cda5da8dSAndroid Build Coastguard Worker    if char == "t":
608*cda5da8dSAndroid Build Coastguard Worker        if src.startswith("true", pos):
609*cda5da8dSAndroid Build Coastguard Worker            return pos + 4, True
610*cda5da8dSAndroid Build Coastguard Worker    if char == "f":
611*cda5da8dSAndroid Build Coastguard Worker        if src.startswith("false", pos):
612*cda5da8dSAndroid Build Coastguard Worker            return pos + 5, False
613*cda5da8dSAndroid Build Coastguard Worker
614*cda5da8dSAndroid Build Coastguard Worker    # Arrays
615*cda5da8dSAndroid Build Coastguard Worker    if char == "[":
616*cda5da8dSAndroid Build Coastguard Worker        return parse_array(src, pos, parse_float)
617*cda5da8dSAndroid Build Coastguard Worker
618*cda5da8dSAndroid Build Coastguard Worker    # Inline tables
619*cda5da8dSAndroid Build Coastguard Worker    if char == "{":
620*cda5da8dSAndroid Build Coastguard Worker        return parse_inline_table(src, pos, parse_float)
621*cda5da8dSAndroid Build Coastguard Worker
622*cda5da8dSAndroid Build Coastguard Worker    # Dates and times
623*cda5da8dSAndroid Build Coastguard Worker    datetime_match = RE_DATETIME.match(src, pos)
624*cda5da8dSAndroid Build Coastguard Worker    if datetime_match:
625*cda5da8dSAndroid Build Coastguard Worker        try:
626*cda5da8dSAndroid Build Coastguard Worker            datetime_obj = match_to_datetime(datetime_match)
627*cda5da8dSAndroid Build Coastguard Worker        except ValueError as e:
628*cda5da8dSAndroid Build Coastguard Worker            raise suffixed_err(src, pos, "Invalid date or datetime") from e
629*cda5da8dSAndroid Build Coastguard Worker        return datetime_match.end(), datetime_obj
630*cda5da8dSAndroid Build Coastguard Worker    localtime_match = RE_LOCALTIME.match(src, pos)
631*cda5da8dSAndroid Build Coastguard Worker    if localtime_match:
632*cda5da8dSAndroid Build Coastguard Worker        return localtime_match.end(), match_to_localtime(localtime_match)
633*cda5da8dSAndroid Build Coastguard Worker
634*cda5da8dSAndroid Build Coastguard Worker    # Integers and "normal" floats.
635*cda5da8dSAndroid Build Coastguard Worker    # The regex will greedily match any type starting with a decimal
636*cda5da8dSAndroid Build Coastguard Worker    # char, so needs to be located after handling of dates and times.
637*cda5da8dSAndroid Build Coastguard Worker    number_match = RE_NUMBER.match(src, pos)
638*cda5da8dSAndroid Build Coastguard Worker    if number_match:
639*cda5da8dSAndroid Build Coastguard Worker        return number_match.end(), match_to_number(number_match, parse_float)
640*cda5da8dSAndroid Build Coastguard Worker
641*cda5da8dSAndroid Build Coastguard Worker    # Special floats
642*cda5da8dSAndroid Build Coastguard Worker    first_three = src[pos : pos + 3]
643*cda5da8dSAndroid Build Coastguard Worker    if first_three in {"inf", "nan"}:
644*cda5da8dSAndroid Build Coastguard Worker        return pos + 3, parse_float(first_three)
645*cda5da8dSAndroid Build Coastguard Worker    first_four = src[pos : pos + 4]
646*cda5da8dSAndroid Build Coastguard Worker    if first_four in {"-inf", "+inf", "-nan", "+nan"}:
647*cda5da8dSAndroid Build Coastguard Worker        return pos + 4, parse_float(first_four)
648*cda5da8dSAndroid Build Coastguard Worker
649*cda5da8dSAndroid Build Coastguard Worker    raise suffixed_err(src, pos, "Invalid value")
650*cda5da8dSAndroid Build Coastguard Worker
651*cda5da8dSAndroid Build Coastguard Worker
652*cda5da8dSAndroid Build Coastguard Workerdef suffixed_err(src: str, pos: Pos, msg: str) -> TOMLDecodeError:
653*cda5da8dSAndroid Build Coastguard Worker    """Return a `TOMLDecodeError` where error message is suffixed with
654*cda5da8dSAndroid Build Coastguard Worker    coordinates in source."""
655*cda5da8dSAndroid Build Coastguard Worker
656*cda5da8dSAndroid Build Coastguard Worker    def coord_repr(src: str, pos: Pos) -> str:
657*cda5da8dSAndroid Build Coastguard Worker        if pos >= len(src):
658*cda5da8dSAndroid Build Coastguard Worker            return "end of document"
659*cda5da8dSAndroid Build Coastguard Worker        line = src.count("\n", 0, pos) + 1
660*cda5da8dSAndroid Build Coastguard Worker        if line == 1:
661*cda5da8dSAndroid Build Coastguard Worker            column = pos + 1
662*cda5da8dSAndroid Build Coastguard Worker        else:
663*cda5da8dSAndroid Build Coastguard Worker            column = pos - src.rindex("\n", 0, pos)
664*cda5da8dSAndroid Build Coastguard Worker        return f"line {line}, column {column}"
665*cda5da8dSAndroid Build Coastguard Worker
666*cda5da8dSAndroid Build Coastguard Worker    return TOMLDecodeError(f"{msg} (at {coord_repr(src, pos)})")
667*cda5da8dSAndroid Build Coastguard Worker
668*cda5da8dSAndroid Build Coastguard Worker
669*cda5da8dSAndroid Build Coastguard Workerdef is_unicode_scalar_value(codepoint: int) -> bool:
670*cda5da8dSAndroid Build Coastguard Worker    return (0 <= codepoint <= 55295) or (57344 <= codepoint <= 1114111)
671*cda5da8dSAndroid Build Coastguard Worker
672*cda5da8dSAndroid Build Coastguard Worker
673*cda5da8dSAndroid Build Coastguard Workerdef make_safe_parse_float(parse_float: ParseFloat) -> ParseFloat:
674*cda5da8dSAndroid Build Coastguard Worker    """A decorator to make `parse_float` safe.
675*cda5da8dSAndroid Build Coastguard Worker
676*cda5da8dSAndroid Build Coastguard Worker    `parse_float` must not return dicts or lists, because these types
677*cda5da8dSAndroid Build Coastguard Worker    would be mixed with parsed TOML tables and arrays, thus confusing
678*cda5da8dSAndroid Build Coastguard Worker    the parser. The returned decorated callable raises `ValueError`
679*cda5da8dSAndroid Build Coastguard Worker    instead of returning illegal types.
680*cda5da8dSAndroid Build Coastguard Worker    """
681*cda5da8dSAndroid Build Coastguard Worker    # The default `float` callable never returns illegal types. Optimize it.
682*cda5da8dSAndroid Build Coastguard Worker    if parse_float is float:  # type: ignore[comparison-overlap]
683*cda5da8dSAndroid Build Coastguard Worker        return float
684*cda5da8dSAndroid Build Coastguard Worker
685*cda5da8dSAndroid Build Coastguard Worker    def safe_parse_float(float_str: str) -> Any:
686*cda5da8dSAndroid Build Coastguard Worker        float_value = parse_float(float_str)
687*cda5da8dSAndroid Build Coastguard Worker        if isinstance(float_value, (dict, list)):
688*cda5da8dSAndroid Build Coastguard Worker            raise ValueError("parse_float must not return dicts or lists")
689*cda5da8dSAndroid Build Coastguard Worker        return float_value
690*cda5da8dSAndroid Build Coastguard Worker
691*cda5da8dSAndroid Build Coastguard Worker    return safe_parse_float
692