1# Copyright 2023 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15from typing import List, Union
16
17from bumble import core
18
19
20class AtParsingError(core.InvalidPacketError):
21    """Error raised when parsing AT commands fails."""
22
23
24def tokenize_parameters(buffer: bytes) -> List[bytes]:
25    """Split input parameters into tokens.
26    Removes space characters outside of double quote blocks:
27    T-rec-V-25 - 5.2.1 Command line general format: "Space characters (IA5 2/0)
28    are ignored [..], unless they are embedded in numeric or string constants"
29    Raises AtParsingError in case of invalid input string."""
30
31    tokens = []
32    in_quotes = False
33    token = bytearray()
34    for b in buffer:
35        char = bytearray([b])
36
37        if in_quotes:
38            token.extend(char)
39            if char == b'\"':
40                in_quotes = False
41                tokens.append(token[1:-1])
42                token = bytearray()
43        else:
44            if char == b' ':
45                pass
46            elif char == b',' or char == b')':
47                tokens.append(token)
48                tokens.append(char)
49                token = bytearray()
50            elif char == b'(':
51                if len(token) > 0:
52                    raise AtParsingError("open_paren following regular character")
53                tokens.append(char)
54            elif char == b'"':
55                if len(token) > 0:
56                    raise AtParsingError("quote following regular character")
57                in_quotes = True
58                token.extend(char)
59            else:
60                token.extend(char)
61
62    tokens.append(token)
63    return [bytes(token) for token in tokens if len(token) > 0]
64
65
66def parse_parameters(buffer: bytes) -> List[Union[bytes, list]]:
67    """Parse the parameters using the comma and parenthesis separators.
68    Raises AtParsingError in case of invalid input string."""
69
70    tokens = tokenize_parameters(buffer)
71    accumulator: List[list] = [[]]
72    current: Union[bytes, list] = bytes()
73
74    for token in tokens:
75        if token == b',':
76            accumulator[-1].append(current)
77            current = bytes()
78        elif token == b'(':
79            accumulator.append([])
80        elif token == b')':
81            if len(accumulator) < 2:
82                raise AtParsingError("close_paren without matching open_paren")
83            accumulator[-1].append(current)
84            current = accumulator.pop()
85        else:
86            current = token
87
88    accumulator[-1].append(current)
89    if len(accumulator) > 1:
90        raise AtParsingError("missing close_paren")
91    return accumulator[0]
92