Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
binding.c620 linesDownload Raw Back to pylsqpack
1#define PY_SSIZE_T_CLEAN
2
3#include <assert.h>
4
5#include <Python.h>
6#include "lsqpack.h"
7#include "lsxpack_header.h"
8
9#define MODULE_NAME "pylsqpack._binding"
10
11#define DEC_BUF_SZ 4096
12#define ENC_BUF_SZ 4096
13#define HDR_BUF_SZ 4096
14#define XHDR_BUF_SZ 4096
15#define PREFIX_MAX_SIZE 16
16
17static PyObject *DecompressionFailed;
18static PyObject *DecoderStreamError;
19static PyObject *DecoderType;
20static PyObject *EncoderStreamError;
21static PyObject *EncoderType;
22static PyObject *StreamBlocked;
23
24struct header_block {
25    STAILQ_ENTRY(header_block) entries;
26
27    int blocked:1;
28    unsigned char *data;
29    size_t data_len;
30    const unsigned char *data_ptr;
31    struct lsxpack_header xhdr;
32    // This buffer is owned by the header_block and is reused internally by xhdr.
33    char *header_buffer;
34    uint64_t stream_id;
35    PyObject *headers;
36};
37
38static struct header_block *header_block_new(size_t stream_id, const unsigned char *data, size_t data_len)
39{
40    struct header_block *hblock = malloc(sizeof(struct header_block));
41    memset(hblock, 0, sizeof(*hblock));
42    hblock->data = malloc(data_len);
43    hblock->data_len = data_len;
44    hblock->data_ptr = hblock->data;
45    memcpy(hblock->data, data, data_len);
46    hblock->stream_id = stream_id;
47    hblock->headers = PyList_New(0);
48    return hblock;
49}
50
51static void header_block_free(struct header_block *hblock)
52{
53    free(hblock->data);
54    hblock->data = 0;
55    hblock->data_ptr = 0;
56    free(hblock->header_buffer);
57    Py_DECREF(hblock->headers);
58    free(hblock);
59}
60
61static void header_block_unblocked(void *opaque) {
62    struct header_block *hblock = opaque;
63    hblock->blocked = 0;
64}
65
66/**
67 * Prepare to decode a header by allocating the requested memory.
68 */
69static struct lsxpack_header *header_block_prepare_decode(void *opaque, struct lsxpack_header *xhdr, size_t space) {
70    struct header_block *hblock = opaque;
71    char *buf;
72
73    // The behaviour of realloc(ptr, 0) is implementation specific,
74    // so if asked for a zero size we explicitly free the memory.
75    if (space) {
76        buf = realloc(hblock->header_buffer, space);
77        if (!buf) return NULL;
78    } else {
79        free(hblock->header_buffer);
80        buf = 0;
81    }
82    hblock->header_buffer = buf;
83
84    if (xhdr) {
85        assert(&hblock->xhdr == xhdr);
86        assert(space > xhdr->val_len);
87
88        xhdr->buf = buf;
89        xhdr->val_len = space;
90    } else {
91        xhdr = &hblock->xhdr;
92        lsxpack_header_prepare_decode(xhdr, buf, 0, space);
93    }
94    return xhdr;
95}
96
97/**
98 * Process a decoded header by appending it to the list of headers.
99 */
100static int header_block_process_header(void *opaque, struct lsxpack_header *xhdr) {
101    struct header_block *hblock = opaque;
102    PyObject *tuple, *name, *value;
103
104    name = PyBytes_FromStringAndSize(lsxpack_header_get_name(xhdr), xhdr->name_len);
105    value = PyBytes_FromStringAndSize(lsxpack_header_get_value(xhdr), xhdr->val_len);
106    tuple = PyTuple_Pack(2, name, value);
107    Py_DECREF(name);
108    Py_DECREF(value);
109
110    PyList_Append(hblock->headers, tuple);
111    Py_DECREF(tuple);
112
113    return 0;
114}
115
116static const struct lsqpack_dec_hset_if header_block_if = {
117    .dhi_unblocked = header_block_unblocked,
118    .dhi_prepare_decode = header_block_prepare_decode,
119    .dhi_process_header = header_block_process_header,
120};
121
122// DECODER
123
124typedef struct {
125    PyObject_HEAD
126    struct lsqpack_dec dec;
127    unsigned char dec_buf[DEC_BUF_SZ];
128    STAILQ_HEAD(, header_block) pending_blocks;
129} DecoderObject;
130
131static int
132Decoder_init(DecoderObject *self, PyObject *args, PyObject *kwargs)
133{
134    char *kwlist[] = {"max_table_capacity", "blocked_streams", NULL};
135    unsigned max_table_capacity, blocked_streams;
136    if (!PyArg_ParseTupleAndKeywords(args, kwargs, "II", kwlist, &max_table_capacity, &blocked_streams))
137        return -1;
138
139    lsqpack_dec_init(&self->dec, NULL, max_table_capacity, blocked_streams, &header_block_if, 0);
140
141    STAILQ_INIT(&self->pending_blocks);
142
143    return 0;
144}
145
146static void
147Decoder_dealloc(DecoderObject *self)
148{
149    struct header_block *hblock;
150
151    lsqpack_dec_cleanup(&self->dec);
152
153    while (!STAILQ_EMPTY(&self->pending_blocks)) {
154        hblock = STAILQ_FIRST(&self->pending_blocks);
155        STAILQ_REMOVE_HEAD(&self->pending_blocks, entries);
156        header_block_free(hblock);
157    }
158
159    PyTypeObject *tp = Py_TYPE(self);
160    freefunc free = PyType_GetSlot(tp, Py_tp_free);
161    free(self);
162    Py_DECREF(tp);
163}
164
165PyDoc_STRVAR(Decoder_feed_encoder__doc__,
166    "feed_encoder(data: bytes) -> List[int]\n\n"
167    "Feed data from the encoder stream.\n\n"
168    "If processing the data unblocked any streams, their IDs are returned, "
169    "and :meth:`resume_header()` must be called for each stream ID.\n\n"
170    "If the data cannot be processed, :class:`EncoderStreamError` is raised.\n\n"
171    ":param data: the encoder stream data\n");
172
173static PyObject*
174Decoder_feed_encoder(DecoderObject *self, PyObject *args, PyObject *kwargs)
175{
176    char *kwlist[] = {"data", NULL};
177    const unsigned char *data;
178    Py_ssize_t data_len;
179    PyObject *list, *value;
180    struct header_block *hblock;
181
182    if (!PyArg_ParseTupleAndKeywords(args, kwargs, "y#", kwlist, &data, &data_len))
183        return NULL;
184
185    if (lsqpack_dec_enc_in(&self->dec, data, data_len) < 0) {
186        PyErr_SetString(EncoderStreamError, "lsqpack_dec_enc_in failed");
187        return NULL;
188    }
189
190    list = PyList_New(0);
191    STAILQ_FOREACH(hblock, &self->pending_blocks, entries) {
192        if (!hblock->blocked) {
193            value = PyLong_FromUnsignedLongLong(hblock->stream_id);
194            PyList_Append(list, value);
195            Py_DECREF(value);
196        }
197    }
198    return list;
199}
200
201PyDoc_STRVAR(Decoder_feed_header__doc__,
202    "feed_header(stream_id: int, data: bytes) -> Tuple[bytes, List[Tuple[bytes, bytes]]]\n\n"
203    "Decode a header block and return control data and headers.\n\n"
204    "If the stream is blocked, :class:`StreamBlocked` is raised.\n\n"
205    "If the data cannot be processed, :class:`DecompressionFailed` is raised.\n\n"
206    ":param stream_id: the ID of the stream\n"
207    ":param data: the header block data\n");
208
209static PyObject*
210Decoder_feed_header(DecoderObject *self, PyObject *args, PyObject *kwargs)
211{
212    char *kwlist[] = {"stream_id", "data", NULL};
213    uint64_t stream_id;
214    const unsigned char *data;
215    Py_ssize_t data_len;
216    PyObject *control, *tuple;
217    size_t dec_len = DEC_BUF_SZ;
218    enum lsqpack_read_header_status status;
219    struct header_block *hblock;
220
221    if (!PyArg_ParseTupleAndKeywords(args, kwargs, "Ky#", kwlist, &stream_id, &data, &data_len))
222        return NULL;
223
224    // check there is no header block for the stream
225    STAILQ_FOREACH(hblock, &self->pending_blocks, entries) {
226        if (hblock->stream_id == stream_id) {
227            PyErr_Format(PyExc_ValueError, "a header block for stream %d already exists", stream_id);
228            return NULL;
229        }
230    }
231    hblock = header_block_new(stream_id, data, data_len);
232
233    status = lsqpack_dec_header_in(
234        &self->dec,
235        hblock,
236        stream_id,
237        hblock->data_len,
238        &hblock->data_ptr,
239        hblock->data_len,
240        self->dec_buf,
241        &dec_len
242    );
243
244    if (status == LQRHS_BLOCKED || status == LQRHS_NEED) {
245        hblock->blocked = 1;
246        STAILQ_INSERT_TAIL(&self->pending_blocks, hblock, entries);
247        PyErr_Format(StreamBlocked, "stream %d is blocked", stream_id);
248        return NULL;
249    } else if (status != LQRHS_DONE) {
250        PyErr_Format(DecompressionFailed, "lsqpack_dec_header_in for stream %d failed", stream_id);
251        header_block_free(hblock);
252        return NULL;
253    }
254
255    control = PyBytes_FromStringAndSize((const char*)self->dec_buf, dec_len);
256    tuple = PyTuple_Pack(2, control, hblock->headers);
257    Py_DECREF(control);
258
259    header_block_free(hblock);
260
261    return tuple;
262}
263
264PyDoc_STRVAR(Decoder_resume_header__doc__,
265    "resume_header(stream_id: int) -> Tuple[bytes, List[Tuple[bytes, bytes]]]\n\n"
266    "Continue decoding a header block and return control data and headers.\n\n"
267    "This method should be called only when :meth:`feed_encoder` indicates "
268    "that a stream has become unblocked\n\n"
269    ":param stream_id: the ID of the stream\n");
270
271static PyObject*
272Decoder_resume_header(DecoderObject *self, PyObject *args, PyObject *kwargs)
273{
274    char *kwlist[] = {"stream_id", NULL};
275    uint64_t stream_id;
276    PyObject *control, *tuple;
277    size_t dec_len = DEC_BUF_SZ;
278    enum lsqpack_read_header_status status;
279    struct header_block *hblock;
280    int found = 0;
281
282    if (!PyArg_ParseTupleAndKeywords(args, kwargs, "K", kwlist, &stream_id))
283        return NULL;
284
285    // find the header block for the stream
286    STAILQ_FOREACH(hblock, &self->pending_blocks, entries) {
287        if (hblock->stream_id == stream_id) {
288            found = 1;
289            break;
290        }
291    }
292    if (!found) {
293        PyErr_Format(PyExc_ValueError, "no pending header block for stream %d", stream_id);
294        return NULL;
295    }
296
297    if (hblock->blocked) {
298        status = LQRHS_BLOCKED;
299    } else {
300        status = lsqpack_dec_header_read(
301            &self->dec,
302            hblock,
303            &hblock->data_ptr,
304            hblock->data_len - (hblock->data_ptr - hblock->data),
305            self->dec_buf,
306            &dec_len
307        );
308    }
309
310    if (status == LQRHS_BLOCKED || status == LQRHS_NEED) {
311        hblock->blocked = 1;
312        PyErr_Format(StreamBlocked, "stream %d is blocked", stream_id);
313        return NULL;
314    } else if (status != LQRHS_DONE) {
315        PyErr_Format(DecompressionFailed, "lsqpack_dec_header_read for stream %d failed (%d)", stream_id, status);
316        STAILQ_REMOVE(&self->pending_blocks, hblock, header_block, entries);
317        header_block_free(hblock);
318        return NULL;
319    }
320
321    control = PyBytes_FromStringAndSize((const char*)self->dec_buf, dec_len);
322    tuple = PyTuple_Pack(2, control, hblock->headers);
323    Py_DECREF(control);
324
325    STAILQ_REMOVE(&self->pending_blocks, hblock, header_block, entries);
326    header_block_free(hblock);
327
328    return tuple;
329}
330
331static PyMethodDef Decoder_methods[] = {
332    {"feed_encoder", (PyCFunction)Decoder_feed_encoder, METH_VARARGS | METH_KEYWORDS, Decoder_feed_encoder__doc__},
333    {"feed_header", (PyCFunction)Decoder_feed_header, METH_VARARGS | METH_KEYWORDS, Decoder_feed_header__doc__},
334    {"resume_header", (PyCFunction)Decoder_resume_header, METH_VARARGS | METH_KEYWORDS, Decoder_resume_header__doc__},
335    {NULL}
336};
337
338PyDoc_STRVAR(Decoder__doc__,
339    "Decoder(max_table_capacity: int, blocked_streams: int)\n\n"
340    "QPACK decoder.\n\n"
341    ":param max_table_capacity: the maximum size in bytes of the dynamic table\n"
342    ":param blocked_streams: the maximum number of streams that could be blocked\n");
343
344static PyType_Slot DecoderType_slots[] = {
345    {Py_tp_dealloc, Decoder_dealloc},
346    {Py_tp_methods, Decoder_methods},
347    {Py_tp_doc, (char *)Decoder__doc__},
348    {Py_tp_init, Decoder_init},
349    {0, 0},
350};
351
352static PyType_Spec DecoderType_spec = {
353    MODULE_NAME ".Decoder",
354    sizeof(DecoderObject),
355    0,
356    Py_TPFLAGS_DEFAULT,
357    DecoderType_slots
358};
359
360// ENCODER
361
362typedef struct {
363    PyObject_HEAD
364    struct lsqpack_enc enc;
365    unsigned char hdr_buf[HDR_BUF_SZ];
366    unsigned char enc_buf[ENC_BUF_SZ];
367    unsigned char pfx_buf[PREFIX_MAX_SIZE];
368    char xhdr_buf[XHDR_BUF_SZ];
369} EncoderObject;
370
371static int
372Encoder_init(EncoderObject *self, PyObject *args, PyObject *kwargs)
373{
374    lsqpack_enc_preinit(&self->enc, NULL);
375    return 0;
376}
377
378static void
379Encoder_dealloc(EncoderObject *self)
380{
381    lsqpack_enc_cleanup(&self->enc);
382
383    PyTypeObject *tp = Py_TYPE(self);
384    freefunc free = PyType_GetSlot(tp, Py_tp_free);
385    free(self);
386    Py_DECREF(tp);
387}
388
389PyDoc_STRVAR(Encoder_apply_settings__doc__,
390    "apply_settings(max_table_capacity: int, blocked_streams: int) -> bytes\n\n"
391    "Apply the settings received from the encoder.\n\n"
392    ":param max_table_capacity: the maximum size in bytes of the dynamic table\n"
393    ":param blocked_streams: the maximum number of streams that could be blocked\n");
394
395static PyObject*
396Encoder_apply_settings(EncoderObject *self, PyObject *args, PyObject *kwargs)
397{
398    char *kwlist[] = {"max_table_capacity", "blocked_streams", NULL};
399    unsigned max_table_capacity, blocked_streams;
400    unsigned char tsu_buf[LSQPACK_LONGEST_SDTC];
401    size_t tsu_len = sizeof(tsu_buf);
402
403    if (!PyArg_ParseTupleAndKeywords(args, kwargs, "II", kwlist, &max_table_capacity, &blocked_streams))
404        return NULL;
405
406    if (lsqpack_enc_init(&self->enc, NULL, max_table_capacity, max_table_capacity, blocked_streams,
407                         LSQPACK_ENC_OPT_STAGE_2, tsu_buf, &tsu_len) != 0) {
408        PyErr_SetString(PyExc_RuntimeError, "lsqpack_enc_init failed");
409        return NULL;
410    }
411
412    return PyBytes_FromStringAndSize((const char*)tsu_buf, tsu_len);
413}
414
415PyDoc_STRVAR(Encoder_encode__doc__,
416    "encode(stream_id: int, headers: List[Tuple[bytes, bytes]]) -> Tuple[bytes, bytes]\n\n"
417    "Encode a list of headers.\n\n"
418    "A tuple is returned containing two bytestrings: the encoder stream data "
419    " and the encoded header block.\n\n"
420    ":param stream_id: the stream ID\n"
421    ":param headers: a list of header tuples\n");
422
423static PyObject*
424Encoder_encode(EncoderObject *self, PyObject *args, PyObject *kwargs)
425{
426    char *kwlist[] = {"stream_id", "headers", NULL};
427    uint64_t stream_id;
428    unsigned seqno = 0;
429    PyObject *list, *tuple, *name, *value;
430    size_t enc_len, hdr_len, pfx_len;
431    size_t enc_off = 0, hdr_off = PREFIX_MAX_SIZE, pfx_off = 0;
432    struct lsxpack_header xhdr;
433    size_t name_len, value_len;
434
435    if (!PyArg_ParseTupleAndKeywords(args, kwargs, "KO", kwlist, &stream_id, &list))
436        return NULL;
437
438    // Validate all the input headers.
439    if (!PyList_Check(list)) {
440        PyErr_SetString(PyExc_ValueError, "headers must be a list");
441        return NULL;
442    }
443
444    for (Py_ssize_t i = 0; i < PyList_Size(list); ++i) {
445        tuple = PyList_GetItem(list, i);
446        if (!PyTuple_Check(tuple) || PyTuple_Size(tuple) != 2) {
447            PyErr_SetString(PyExc_ValueError, "the header must be a two-tuple");
448            return NULL;
449        }
450        name = PyTuple_GetItem(tuple, 0);
451        value = PyTuple_GetItem(tuple, 1);
452        if (!PyBytes_Check(name) || !PyBytes_Check(value)) {
453            PyErr_SetString(PyExc_ValueError, "the header's name and value must be bytes");
454            return NULL;
455        }
456        name_len = PyBytes_Size(name);
457        value_len = PyBytes_Size(value);
458        if (name_len == 0) {
459            PyErr_SetString(PyExc_ValueError, "the header's name must not be empty");
460            return NULL;
461        }
462        if (name_len + value_len > XHDR_BUF_SZ) {
463            PyErr_SetString(PyExc_ValueError, "the header's name and value are too long");
464            return NULL;
465        }
466    }
467
468    // Start the encoding transaction.
469    if (lsqpack_enc_start_header(&self->enc, stream_id, seqno) != 0) {
470        PyErr_SetString(PyExc_RuntimeError, "lsqpack_enc_start_header failed");
471        return NULL;
472    }
473
474    for (Py_ssize_t i = 0; i < PyList_Size(list); ++i) {
475        tuple = PyList_GetItem(list, i);
476        name = PyTuple_GetItem(tuple, 0);
477        value = PyTuple_GetItem(tuple, 1);
478        name_len = PyBytes_Size(name);
479        value_len = PyBytes_Size(value);
480
481        // Copy the header name and value into the xhdr buffer.
482        memcpy(self->xhdr_buf, PyBytes_AsString(name), name_len);
483        memcpy(self->xhdr_buf + name_len, PyBytes_AsString(value), value_len);
484        lsxpack_header_set_offset2(&xhdr, self->xhdr_buf, 0, name_len, name_len, value_len);
485
486        enc_len = ENC_BUF_SZ - enc_off;
487        hdr_len = HDR_BUF_SZ - hdr_off;
488        if (lsqpack_enc_encode(&self->enc,
489                               self->enc_buf + enc_off, &enc_len,
490                               self->hdr_buf + hdr_off, &hdr_len,
491                               &xhdr,
492                               0) != LQES_OK) {
493            PyErr_SetString(PyExc_RuntimeError, "lsqpack_enc_encode failed");
494            lsqpack_enc_end_header(&self->enc, self->pfx_buf, PREFIX_MAX_SIZE, NULL);
495            return NULL;
496        }
497        enc_off += enc_len;
498        hdr_off += hdr_len;
499    }
500
501    pfx_len = lsqpack_enc_end_header(&self->enc, self->pfx_buf, PREFIX_MAX_SIZE, NULL);
502    if (pfx_len <= 0) {
503        PyErr_SetString(PyExc_RuntimeError, "lsqpack_enc_end_header failed");
504        return NULL;
505    }
506    pfx_off = PREFIX_MAX_SIZE - pfx_len;
507    memcpy(self->hdr_buf + pfx_off, self->pfx_buf, pfx_len);
508
509    name = PyBytes_FromStringAndSize((const char*)self->enc_buf, enc_off);
510    value = PyBytes_FromStringAndSize((const char*)self->hdr_buf + pfx_off, hdr_off - pfx_off);
511    tuple = PyTuple_Pack(2, name, value);
512    Py_DECREF(name);
513    Py_DECREF(value);
514
515    return tuple;
516}
517
518PyDoc_STRVAR(Encoder_feed_decoder__doc__,
519    "feed_decoder(data: bytes) -> None\n\n"
520    "Feed data from the decoder stream.\n\n"
521    "If the data cannot be processed, :class:`DecoderStreamError` is raised.\n\n"
522    ":param data: the decoder stream data\n");
523
524static PyObject*
525Encoder_feed_decoder(EncoderObject *self, PyObject *args, PyObject *kwargs)
526{
527    char *kwlist[] = {"data", NULL};
528    const unsigned char *data;
529    Py_ssize_t data_len;
530
531    if (!PyArg_ParseTupleAndKeywords(args, kwargs, "y#", kwlist, &data, &data_len))
532        return NULL;
533
534    if (lsqpack_enc_decoder_in(&self->enc, data, data_len) < 0) {
535        PyErr_SetString(DecoderStreamError, "lsqpack_enc_decoder_in failed");
536        return NULL;
537    }
538
539    Py_RETURN_NONE;
540}
541
542static PyMethodDef Encoder_methods[] = {
543    {"apply_settings", (PyCFunction)Encoder_apply_settings, METH_VARARGS | METH_KEYWORDS, Encoder_apply_settings__doc__},
544    {"encode", (PyCFunction)Encoder_encode, METH_VARARGS | METH_KEYWORDS, Encoder_encode__doc__},
545    {"feed_decoder", (PyCFunction)Encoder_feed_decoder, METH_VARARGS | METH_KEYWORDS, Encoder_feed_decoder__doc__},
546    {NULL}
547};
548
549PyDoc_STRVAR(Encoder__doc__,
550    "Encoder()\n\n"
551    "QPACK encoder.\n");
552
553static PyType_Slot EncoderType_slots[] = {
554    {Py_tp_dealloc, Encoder_dealloc},
555    {Py_tp_methods, Encoder_methods},
556    {Py_tp_doc, (char *)Encoder__doc__},
557    {Py_tp_init, Encoder_init},
558    {0, 0},
559};
560
561static PyType_Spec EncoderType_spec = {
562    MODULE_NAME ".Encoder",
563    sizeof(EncoderObject),
564    0,
565    Py_TPFLAGS_DEFAULT,
566    EncoderType_slots
567};
568
569// MODULE
570
571static struct PyModuleDef moduledef = {
572    PyModuleDef_HEAD_INIT,
573    MODULE_NAME,                        /* m_name */
574    "Bindings for ls-qpack.",           /* m_doc */
575    -1,                                 /* m_size */
576    NULL,                               /* m_methods */
577    NULL,                               /* m_reload */
578    NULL,                               /* m_traverse */
579    NULL,                               /* m_clear */
580    NULL,                               /* m_free */
581};
582
583PyMODINIT_FUNC
584PyInit__binding(void)
585{
586    PyObject* m;
587
588    m = PyModule_Create(&moduledef);
589    if (m == NULL)
590        return NULL;
591
592    DecompressionFailed = PyErr_NewException(MODULE_NAME ".DecompressionFailed", PyExc_ValueError, NULL);
593    Py_INCREF(DecompressionFailed);
594    PyModule_AddObject(m, "DecompressionFailed", DecompressionFailed);
595
596    DecoderStreamError = PyErr_NewException(MODULE_NAME ".DecoderStreamError", PyExc_ValueError, NULL);
597    Py_INCREF(DecoderStreamError);
598    PyModule_AddObject(m, "DecoderStreamError", DecoderStreamError);
599
600    EncoderStreamError = PyErr_NewException(MODULE_NAME ".EncoderStreamError", PyExc_ValueError, NULL);
601    Py_INCREF(EncoderStreamError);
602    PyModule_AddObject(m, "EncoderStreamError", EncoderStreamError);
603
604    StreamBlocked = PyErr_NewException(MODULE_NAME ".StreamBlocked", PyExc_ValueError, NULL);
605    Py_INCREF(StreamBlocked);
606    PyModule_AddObject(m, "StreamBlocked", StreamBlocked);
607
608    DecoderType = PyType_FromSpec(&DecoderType_spec);
609    if (DecoderType == NULL)
610        return NULL;
611    PyModule_AddObject(m, "Decoder", DecoderType);
612
613    EncoderType = PyType_FromSpec(&EncoderType_spec);
614    if (EncoderType == NULL)
615        return NULL;
616    PyModule_AddObject(m, "Encoder", EncoderType);
617
618    return m;
619}
620 
codekingpro/portable-devtools · Team Ai