1#!/usr/local/bin/python3.8
2# vim:fileencoding=utf-8
3# License: GPLv3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
4
5import subprocess
6from collections import defaultdict
7from typing import Any, DefaultDict, Dict, FrozenSet, List, Tuple, Union
8
9KeymapType = Dict[str, Tuple[str, Union[FrozenSet[str], str]]]
10
11
12def resolve_keys(keymap: KeymapType) -> DefaultDict[str, List[str]]:
13    ans: DefaultDict[str, List[str]] = defaultdict(list)
14    for ch, (attr, atype) in keymap.items():
15        if isinstance(atype, str) and atype in ('int', 'uint'):
16            q = atype
17        else:
18            q = 'flag'
19        ans[q].append(ch)
20    return ans
21
22
23def enum(keymap: KeymapType) -> str:
24    lines = []
25    for ch, (attr, atype) in keymap.items():
26        lines.append(f"{attr}='{ch}'")
27    return '''
28    enum KEYS {{
29        {}
30    }};
31    '''.format(',\n'.join(lines))
32
33
34def parse_key(keymap: KeymapType) -> str:
35    lines = []
36    for attr, atype in keymap.values():
37        vs = atype.upper() if isinstance(atype, str) and atype in ('uint', 'int') else 'FLAG'
38        lines.append(f'case {attr}: value_state = {vs}; break;')
39    return '        \n'.join(lines)
40
41
42def parse_flag(keymap: KeymapType, type_map: Dict[str, Any], command_class: str) -> str:
43    lines = []
44    for ch in type_map['flag']:
45        attr, allowed_values = keymap[ch]
46        q = ' && '.join(f"g.{attr} != '{x}'" for x in allowed_values)
47        lines.append(f'''
48            case {attr}: {{
49                g.{attr} = screen->parser_buf[pos++] & 0xff;
50                if ({q}) {{
51                    REPORT_ERROR("Malformed {command_class} control block, unknown flag value for {attr}: 0x%x", g.{attr});
52                    return;
53                }};
54            }}
55            break;
56        ''')
57    return '        \n'.join(lines)
58
59
60def parse_number(keymap: KeymapType) -> Tuple[str, str]:
61    int_keys = [f'I({attr})' for attr, atype in keymap.values() if atype == 'int']
62    uint_keys = [f'U({attr})' for attr, atype in keymap.values() if atype == 'uint']
63    return '; '.join(int_keys), '; '.join(uint_keys)
64
65
66def cmd_for_report(report_name: str, keymap: KeymapType, type_map: Dict[str, Any], payload_allowed: bool) -> str:
67    def group(atype: str, conv: str) -> Tuple[str, str]:
68        flag_fmt, flag_attrs = [], []
69        cv = {'flag': 'c', 'int': 'i', 'uint': 'I'}[atype]
70        for ch in type_map[atype]:
71            flag_fmt.append('s' + cv)
72            attr = keymap[ch][0]
73            flag_attrs.append(f'"{attr}", {conv}g.{attr}')
74        return ' '.join(flag_fmt), ', '.join(flag_attrs)
75
76    flag_fmt, flag_attrs = group('flag', '')
77    int_fmt, int_attrs = group('int', '(int)')
78    uint_fmt, uint_attrs = group('uint', '(unsigned int)')
79
80    fmt = f'{flag_fmt} {uint_fmt} {int_fmt}'
81    if payload_allowed:
82        ans = [f'REPORT_VA_COMMAND("s {{{fmt} sI}} y#", "{report_name}",']
83    else:
84        ans = [f'REPORT_VA_COMMAND("s {{{fmt}}}", "{report_name}",']
85    ans.append(',\n     '.join((flag_attrs, uint_attrs, int_attrs)))
86    if payload_allowed:
87        ans.append(', "payload_sz", g.payload_sz, payload, g.payload_sz')
88    ans.append(');')
89    return '\n'.join(ans)
90
91
92def generate(
93    function_name: str,
94    callback_name: str,
95    report_name: str,
96    keymap: KeymapType,
97    command_class: str,
98    initial_key: str = 'a',
99    payload_allowed: bool = True
100) -> str:
101    type_map = resolve_keys(keymap)
102    keys_enum = enum(keymap)
103    handle_key = parse_key(keymap)
104    flag_keys = parse_flag(keymap, type_map, command_class)
105    int_keys, uint_keys = parse_number(keymap)
106    report_cmd = cmd_for_report(report_name, keymap, type_map, payload_allowed)
107    if payload_allowed:
108        payload_after_value = "case ';': state = PAYLOAD; break;"
109        payload = ', PAYLOAD'
110        parr = 'static uint8_t payload[4096];'
111        payload_case = f'''
112            case PAYLOAD: {{
113                sz = screen->parser_buf_pos - pos;
114                const char *err = base64_decode(screen->parser_buf + pos, sz, payload, sizeof(payload), &g.payload_sz);
115                if (err != NULL) {{ REPORT_ERROR("Failed to parse {command_class} command payload with error: %s", err); return; }}
116                pos = screen->parser_buf_pos;
117                }}
118                break;
119        '''
120        callback = f'{callback_name}(screen, &g, payload)'
121    else:
122        payload_after_value = payload = parr = payload_case = ''
123        callback = f'{callback_name}(screen, &g)'
124
125    return f'''
126static inline void
127{function_name}(Screen *screen, PyObject UNUSED *dump_callback) {{
128    unsigned int pos = 1;
129    enum PARSER_STATES {{ KEY, EQUAL, UINT, INT, FLAG, AFTER_VALUE {payload} }};
130    enum PARSER_STATES state = KEY, value_state = FLAG;
131    static {command_class} g;
132    unsigned int i, code;
133    uint64_t lcode;
134    bool is_negative;
135    memset(&g, 0, sizeof(g));
136    size_t sz;
137    {parr}
138    {keys_enum}
139    enum KEYS key = '{initial_key}';
140    if (screen->parser_buf[pos] == ';') state = AFTER_VALUE;
141
142    while (pos < screen->parser_buf_pos) {{
143        switch(state) {{
144            case KEY:
145                key = screen->parser_buf[pos++];
146                state = EQUAL;
147                switch(key) {{
148                    {handle_key}
149                    default:
150                        REPORT_ERROR("Malformed {command_class} control block, invalid key character: 0x%x", key);
151                        return;
152                }}
153                break;
154
155            case EQUAL:
156                if (screen->parser_buf[pos++] != '=') {{
157                    REPORT_ERROR("Malformed {command_class} control block, no = after key, found: 0x%x instead", screen->parser_buf[pos-1]);
158                    return;
159                }}
160                state = value_state;
161                break;
162
163            case FLAG:
164                switch(key) {{
165                    {flag_keys}
166                    default:
167                        break;
168                }}
169                state = AFTER_VALUE;
170                break;
171
172            case INT:
173#define READ_UINT \\
174                for (i = pos; i < MIN(screen->parser_buf_pos, pos + 10); i++) {{ \\
175                    if (screen->parser_buf[i] < '0' || screen->parser_buf[i] > '9') break; \\
176                }} \\
177                if (i == pos) {{ REPORT_ERROR("Malformed {command_class} control block, expecting an integer value for key: %c", key & 0xFF); return; }} \\
178                lcode = utoi(screen->parser_buf + pos, i - pos); pos = i; \\
179                if (lcode > UINT32_MAX) {{ REPORT_ERROR("Malformed {command_class} control block, number is too large"); return; }} \\
180                code = lcode;
181
182                is_negative = false;
183                if(screen->parser_buf[pos] == '-') {{ is_negative = true; pos++; }}
184#define I(x) case x: g.x = is_negative ? 0 - (int32_t)code : (int32_t)code; break
185                READ_UINT;
186                switch(key) {{
187                    {int_keys};
188                    default: break;
189                }}
190                state = AFTER_VALUE;
191                break;
192#undef I
193            case UINT:
194                READ_UINT;
195#define U(x) case x: g.x = code; break
196                switch(key) {{
197                    {uint_keys};
198                    default: break;
199                }}
200                state = AFTER_VALUE;
201                break;
202#undef U
203#undef READ_UINT
204
205            case AFTER_VALUE:
206                switch (screen->parser_buf[pos++]) {{
207                    default:
208                        REPORT_ERROR("Malformed {command_class} control block, expecting a comma or semi-colon after a value, found: 0x%x",
209                                     screen->parser_buf[pos - 1]);
210                        return;
211                    case ',':
212                        state = KEY;
213                        break;
214                    {payload_after_value}
215                }}
216                break;
217
218            {payload_case}
219
220        }} // end switch
221    }} // end while
222
223    switch(state) {{
224        case EQUAL:
225            REPORT_ERROR("Malformed {command_class} control block, no = after key"); return;
226        case INT:
227        case UINT:
228            REPORT_ERROR("Malformed {command_class} control block, expecting an integer value"); return;
229        case FLAG:
230            REPORT_ERROR("Malformed {command_class} control block, expecting a flag value"); return;
231        default:
232            break;
233    }}
234
235    {report_cmd}
236
237    {callback};
238}}
239    '''
240
241
242def write_header(text: str, path: str) -> None:
243    with open(path, 'w') as f:
244        print(f'// This file is generated by {__file__} do not edit!', file=f, end='\n\n')
245        print('#pragma once', file=f)
246        print(text, file=f)
247    subprocess.check_call(['clang-format', '-i', path])
248
249
250def graphics_parser() -> None:
251    flag = frozenset
252    keymap: KeymapType = {
253        'a': ('action', flag('tTqpdfac')),
254        'd': ('delete_action', flag('aAiIcCfFnNpPqQxXyYzZ')),
255        't': ('transmission_type', flag('dfts')),
256        'o': ('compressed', flag('z')),
257        'f': ('format', 'uint'),
258        'm': ('more', 'uint'),
259        'i': ('id', 'uint'),
260        'I': ('image_number', 'uint'),
261        'p': ('placement_id', 'uint'),
262        'q': ('quiet', 'uint'),
263        'w': ('width', 'uint'),
264        'h': ('height', 'uint'),
265        'x': ('x_offset', 'uint'),
266        'y': ('y_offset', 'uint'),
267        'v': ('data_height', 'uint'),
268        's': ('data_width', 'uint'),
269        'S': ('data_sz', 'uint'),
270        'O': ('data_offset', 'uint'),
271        'c': ('num_cells', 'uint'),
272        'r': ('num_lines', 'uint'),
273        'X': ('cell_x_offset', 'uint'),
274        'Y': ('cell_y_offset', 'uint'),
275        'z': ('z_index', 'int'),
276        'C': ('cursor_movement', 'uint'),
277    }
278    text = generate('parse_graphics_code', 'screen_handle_graphics_command', 'graphics_command', keymap, 'GraphicsCommand')
279    write_header(text, 'kitty/parse-graphics-command.h')
280
281
282graphics_parser()
283