bencode2 0.3.27__cp313-cp313-win_arm64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Binary file
bencode2/__decoder.py ADDED
@@ -0,0 +1,206 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Final
4
+
5
+ from typing_extensions import Buffer
6
+
7
+ char_l: Final = ord("l")
8
+ char_i: Final = ord("i")
9
+ char_e: Final = ord("e")
10
+ char_d: Final = ord("d")
11
+ char_0: Final = ord("0")
12
+ char_9: Final = ord("9")
13
+ char_dash: Final = ord("-")
14
+ char_colon: Final = ord(":")
15
+
16
+
17
+ class BencodeDecodeError(ValueError):
18
+ """Bencode decode error."""
19
+
20
+
21
+ def bdecode(value: Buffer, /) -> Any:
22
+ """Decode bencode formatted bytes to python value."""
23
+ return Decoder(memoryview(value).cast("B")).decode()
24
+
25
+
26
+ class Decoder:
27
+ value: memoryview
28
+ index: int
29
+ size: int
30
+
31
+ __slots__ = ("value", "index", "size")
32
+
33
+ def __init__(self, value: memoryview) -> None:
34
+ self.size = len(value)
35
+ if self.size == 0:
36
+ raise BencodeDecodeError("empty input")
37
+
38
+ self.value = value
39
+ self.index = 0
40
+
41
+ def decode(self) -> object:
42
+ data = self.__decode()
43
+
44
+ if self.index != self.size: # pragma: no cover
45
+ raise BencodeDecodeError("invalid bencode value (data after valid prefix)")
46
+
47
+ return data
48
+
49
+ def __decode(self) -> object:
50
+ if char_0 <= self.value[self.index] <= char_9:
51
+ return self.__decode_bytes()
52
+ if self.value[self.index] == char_i:
53
+ return self.__decode_int()
54
+ if self.value[self.index] == char_d:
55
+ return self.__decode_dict()
56
+ if self.value[self.index] == char_l:
57
+ return self.__decode_list()
58
+
59
+ raise BencodeDecodeError(
60
+ f"unexpected token {self.value[self.index:self.index + 1].tobytes()}. "
61
+ f"index {self.index}"
62
+ )
63
+
64
+ def __decode_int(self) -> int:
65
+ self.index += 1
66
+ for i, c in enumerate(self.value[self.index :]):
67
+ if c == char_e:
68
+ index_end = i + self.index
69
+ break
70
+ else:
71
+ raise BencodeDecodeError(
72
+ f"invalid int, failed to found end. index {self.index}"
73
+ )
74
+
75
+ if index_end == self.index:
76
+ raise BencodeDecodeError(f"invalid int, found 'ie': {self.index}")
77
+
78
+ n: int = 1
79
+ offset: int = 0
80
+
81
+ if self.value[self.index] == char_dash:
82
+ n = -1
83
+ offset = 1
84
+
85
+ total: int = 0
86
+ for c in self.value[self.index + offset : index_end]:
87
+ if not (char_0 <= c <= char_9):
88
+ raise BencodeDecodeError(
89
+ f"malformed int {self.value[self.index:index_end].tobytes()}. index {self.index}"
90
+ )
91
+ total = total * 10 + (c - char_0)
92
+
93
+ n = total * n
94
+
95
+ if self.value[self.index] == char_dash:
96
+ if self.value[self.index + 1] == char_0:
97
+ raise BencodeDecodeError(
98
+ f"-0 is not allowed in bencoding. index: {self.index}"
99
+ )
100
+ elif self.value[self.index] == char_0 and index_end != self.index + 1:
101
+ raise BencodeDecodeError(
102
+ f"integer with leading zero is not allowed. index: {self.index}"
103
+ )
104
+ self.index = index_end + 1
105
+ return n
106
+
107
+ def __decode_list(self) -> list[Any]:
108
+ r: list[Any] = []
109
+ self.index += 1
110
+
111
+ while True:
112
+ if self.index >= self.size:
113
+ raise BencodeDecodeError(
114
+ f"buffer overflow when decoding array, index {self.index}"
115
+ )
116
+
117
+ if self.value[self.index] == char_e:
118
+ break
119
+
120
+ v = self.__decode()
121
+ r.append(v)
122
+
123
+ self.index += 1
124
+ return r
125
+
126
+ def __decode_bytes(self) -> bytes:
127
+ for i, c in enumerate(self.value[self.index :]):
128
+ if c == char_colon:
129
+ index_colon = i + self.index
130
+ break
131
+ else:
132
+ raise BencodeDecodeError(
133
+ f"invalid bytes, failed find expected char ':'. index {self.index}"
134
+ )
135
+
136
+ if self.value[self.index] == char_0:
137
+ if index_colon != self.index + 1:
138
+ raise BencodeDecodeError(
139
+ f"malformed str/bytes length with leading 0. index {self.index}"
140
+ )
141
+
142
+ n: int = 0
143
+ for c in self.value[self.index : index_colon]:
144
+ if not (char_0 <= c <= char_9):
145
+ raise BencodeDecodeError(
146
+ f"malformed str/bytes length {self.value[self.index:index_colon].tobytes()!r}."
147
+ f" index {self.index}"
148
+ )
149
+
150
+ n = n * 10 + (c - char_0)
151
+
152
+ if index_colon + n >= self.size:
153
+ raise BencodeDecodeError(
154
+ f"malformed str/bytes length, buffer overflow. index {self.index}"
155
+ )
156
+
157
+ index_colon += 1
158
+
159
+ s = self.value[index_colon : index_colon + n]
160
+
161
+ self.index = index_colon + n
162
+
163
+ return s.tobytes()
164
+
165
+ def __decode_dict(self) -> dict[str | bytes, Any]:
166
+ start_index = self.index
167
+ self.index += 1
168
+
169
+ items: list[tuple[bytes, Any]] = []
170
+
171
+ while True:
172
+ if self.index >= self.size:
173
+ raise BencodeDecodeError(
174
+ f"buffer overflow when decoding bytes, index {self.index}"
175
+ )
176
+ if self.value[self.index] == char_e:
177
+ break
178
+ if not (char_0 <= self.value[self.index] <= char_9):
179
+ raise BencodeDecodeError(
180
+ f"directory only allow str as keys, "
181
+ f"found unexpected char "
182
+ f"'{self.value[self.index]:c}', index {self.index}"
183
+ )
184
+ k = self.__decode_bytes()
185
+ v = self.__decode()
186
+ items.append((k, v))
187
+
188
+ if not items:
189
+ self.index += 1
190
+ return {}
191
+
192
+ _check_sorted(items, start_index)
193
+
194
+ self.index += 1
195
+
196
+ return dict(items)
197
+
198
+
199
+ def _check_sorted(s: list[tuple[bytes, Any]], idx: int) -> None:
200
+ i = 1
201
+ while i < len(s):
202
+ if s[i][0] < s[i - 1][0]:
203
+ raise BencodeDecodeError(f"directory keys is not sorted, index {idx}")
204
+ if s[i][0] == s[i - 1][0]:
205
+ raise BencodeDecodeError(f"found duplicated keys in directory, index {idx}")
206
+ i += 1
bencode2/__encoder.py ADDED
@@ -0,0 +1,159 @@
1
+ from __future__ import annotations
2
+
3
+ import io
4
+ from collections import OrderedDict
5
+ from collections.abc import Mapping
6
+ from dataclasses import fields, is_dataclass
7
+ from types import MappingProxyType
8
+ from typing import Any
9
+
10
+
11
+ class BencodeEncodeError(ValueError):
12
+ """Bencode encode error."""
13
+
14
+
15
+ def bencode(value: Any, /) -> bytes:
16
+ """Encode value into the bencode format."""
17
+ with io.BytesIO() as w:
18
+ __encode(w, value, set(), stack_depth=0)
19
+ return w.getvalue()
20
+
21
+
22
+ def __encode(w: io.BytesIO, value: Any, seen: set[int], stack_depth: int) -> None:
23
+ if isinstance(value, str):
24
+ return __encode_bytes(w, value.encode("UTF-8"))
25
+
26
+ if isinstance(value, int):
27
+ w.write(b"i")
28
+ # will handle bool and enum.IntEnum
29
+ w.write(str(int(value)).encode())
30
+ w.write(b"e")
31
+ return
32
+
33
+ if isinstance(value, bytes):
34
+ return __encode_bytes(w, value)
35
+
36
+ stack_depth += 1
37
+
38
+ i = id(value)
39
+ if isinstance(value, (dict, OrderedDict, MappingProxyType)):
40
+ if stack_depth >= 100:
41
+ if i in seen:
42
+ raise BencodeEncodeError(f"circular reference found {value!r}")
43
+ seen.add(i)
44
+ __encode_mapping(w, value, seen, stack_depth=stack_depth)
45
+ if stack_depth >= 100: # pragma: no cover
46
+ seen.remove(i)
47
+ stack_depth -= 1
48
+ return
49
+
50
+ if isinstance(value, (list, tuple)):
51
+ if stack_depth >= 100:
52
+ if i in seen:
53
+ raise BencodeEncodeError(f"circular reference found {value!r}")
54
+ seen.add(i)
55
+
56
+ w.write(b"l")
57
+ for item in value:
58
+ __encode(w, item, seen, stack_depth=stack_depth)
59
+ w.write(b"e")
60
+
61
+ if stack_depth >= 100: # pragma: no cover
62
+ seen.remove(i)
63
+ stack_depth -= 1
64
+
65
+ return
66
+
67
+ if isinstance(value, bytearray):
68
+ __encode_bytes(w, bytes(value))
69
+ return
70
+
71
+ if isinstance(value, memoryview):
72
+ w.write(str(len(value)).encode())
73
+ w.write(b":")
74
+ w.write(value)
75
+ return
76
+
77
+ if is_dataclass(value) and not isinstance(value, type):
78
+ if stack_depth >= 100:
79
+ if i in seen:
80
+ raise BencodeEncodeError(f"circular reference found {value!r}")
81
+ seen.add(i)
82
+
83
+ __encode_dataclass(w, value, seen, stack_depth=stack_depth)
84
+
85
+ if stack_depth >= 100: # pragma: no cover
86
+ seen.remove(i)
87
+ stack_depth -= 1
88
+
89
+ return
90
+
91
+ raise TypeError(f"type '{type(value)!r}' not supported by bencode")
92
+
93
+
94
+ def __encode_bytes(w: io.BytesIO, val: bytes) -> None:
95
+ w.write(str(len(val)).encode())
96
+ w.write(b":")
97
+ w.write(val)
98
+
99
+
100
+ def __encode_mapping(
101
+ w: io.BytesIO,
102
+ val: Mapping[Any, Any],
103
+ seen: set[int],
104
+ stack_depth: int,
105
+ ) -> None:
106
+ w.write(b"d")
107
+
108
+ # force all keys to bytes, because str and bytes are incomparable
109
+ i_list: list[tuple[bytes, object]] = [(to_binary(k), v) for k, v in val.items()]
110
+ if not i_list:
111
+ w.write(b"e")
112
+ return
113
+ i_list.sort(key=lambda kv: kv[0])
114
+ __check_duplicated_keys(i_list)
115
+
116
+ for k, v in i_list:
117
+ __encode_bytes(w, k)
118
+ __encode(w, v, seen, stack_depth=stack_depth)
119
+
120
+ w.write(b"e")
121
+
122
+
123
+ def __encode_dataclass(w: io.BytesIO, x: Any, seen: set[int], stack_depth: int) -> None:
124
+ keys = fields(x)
125
+ if not keys:
126
+ w.write(b"de")
127
+ return
128
+
129
+ w.write(b"d")
130
+
131
+ ks = sorted([k.name for k in keys])
132
+
133
+ # no need to check duplicated keys, dataclasses will check this.
134
+
135
+ for k in ks:
136
+ __encode_bytes(w, k.encode())
137
+ __encode(w, getattr(x, k), seen, stack_depth=stack_depth)
138
+
139
+ w.write(b"e")
140
+
141
+
142
+ def __check_duplicated_keys(s: list[tuple[bytes, object]]) -> None:
143
+ last_key: bytes = s[0][0]
144
+ for current, _ in s[1:]:
145
+ if last_key == current:
146
+ raise BencodeEncodeError(
147
+ f"find duplicated keys {last_key!r} and {current.decode()}"
148
+ )
149
+ last_key = current
150
+
151
+
152
+ def to_binary(s: str | bytes) -> bytes:
153
+ if isinstance(s, bytes):
154
+ return s
155
+
156
+ if isinstance(s, str):
157
+ return s.encode("utf-8", "strict")
158
+
159
+ raise TypeError(f"expected binary or text (found {type(s)})")
bencode2/__init__.py ADDED
@@ -0,0 +1,17 @@
1
+ try:
2
+ from .__bencode import BencodeDecodeError, BencodeEncodeError, bdecode, bencode
3
+
4
+ COMPILED = True
5
+ except ModuleNotFoundError:
6
+ from .__decoder import BencodeDecodeError, bdecode
7
+ from .__encoder import BencodeEncodeError, bencode
8
+
9
+ COMPILED = False
10
+
11
+ __all__ = (
12
+ "BencodeDecodeError",
13
+ "BencodeEncodeError",
14
+ "bencode",
15
+ "bdecode",
16
+ "COMPILED",
17
+ )
bencode2/__init__.pyi ADDED
@@ -0,0 +1,8 @@
1
+ from .__bencode import bdecode, bencode
2
+
3
+ __all__ = ["bencode", "bdecode", "BencodeDecodeError", "BencodeEncodeError", "COMPILED"]
4
+
5
+ class BencodeDecodeError(ValueError): ...
6
+ class BencodeEncodeError(ValueError): ...
7
+
8
+ COMPILED: bool = ...
bencode2/py.typed ADDED
File without changes
@@ -0,0 +1,153 @@
1
+ Metadata-Version: 2.1
2
+ Name: bencode2
3
+ Version: 0.3.27
4
+ Summary: A fast and correct bencode serialize/deserialize library
5
+ Keywords: bencode,bittorrent,bit-torrent,serialize,deserialize,p2p
6
+ Author-Email: trim21 <trim21me@gmail.com>
7
+ License: MIT
8
+ Classifier: Development Status :: 5 - Production/Stable
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Project-URL: Homepage, https://github.com/trim21/bencode-py
14
+ Project-URL: Repository, https://github.com/trim21/bencode-py
15
+ Project-URL: Issues, https://github.com/trim21/bencode-py/issues
16
+ Requires-Python: <4.0,>=3.9
17
+ Requires-Dist: typing_extensions>=4.8.0
18
+ Description-Content-Type: text/markdown
19
+
20
+ # A fast and correct bencode serialize/deserialize library
21
+
22
+ [![PyPI](https://img.shields.io/pypi/v/bencode2)](https://pypi.org/project/bencode2/)
23
+ [![tests](https://github.com/trim21/bencode-py/actions/workflows/tests.yaml/badge.svg)](https://github.com/trim21/bencode-py/actions/workflows/tests.yaml)
24
+ [![CircleCI](https://dl.circleci.com/status-badge/img/gh/trim21/bencode-py/tree/master.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/trim21/bencode-py/tree/master)
25
+ [![PyPI - Python Version](https://img.shields.io/badge/python-%3E%3D3.8%2C%3C4.0-blue)](https://pypi.org/project/bencode2/)
26
+ [![Codecov branch](https://img.shields.io/codecov/c/github/Trim21/bencode-py/master)](https://codecov.io/gh/Trim21/bencode-py/branch/master)
27
+
28
+ ## introduction
29
+
30
+ Why yet another bencode package in python?
31
+
32
+ because I need a bencode library:
33
+
34
+ ### 1. Correct
35
+
36
+ It should fully validate its inputs, both encoded bencode bytes, or python object to be
37
+ encoded.
38
+
39
+ And it should not decode bencode bytes to `str` by default.
40
+
41
+ Bencode doesn't have a utf-8 str type, only bytes,
42
+ so many decoder try to decode bytes to str and fallback to bytes,
43
+ **this package won't, it parse bencode bytes value as python bytes.**
44
+
45
+ It may be attempting to parse all dictionary keys as string,
46
+ but for BitTorrent v2 torrent, the keys in `pieces root` dictionary is still sha256 hash
47
+ instead of ascii/utf-8 string.
48
+
49
+ If you prefer string as dictionary keys, write a dedicated function to convert parsing
50
+ result.
51
+
52
+ Also be careful! Even file name or torrent name may not be valid utf-8 string.
53
+
54
+ ### 2. Fast enough
55
+
56
+ this package is written with c++ in CPython.
57
+
58
+ ### 3. still cross implement
59
+
60
+ This package sill have a pure python wheel `bencode2-${version}-py3-none-any.whl` wheel
61
+ on pypi.
62
+
63
+ Which means you can still use it in non-cpython python with same behavior.
64
+
65
+ ## install
66
+
67
+ ### pypi
68
+
69
+ ```shell
70
+ pip install bencode2
71
+ ```
72
+
73
+ ### conda/pixi
74
+
75
+ you can install conda package `bencode2` from <https://prefix.dev/channels/trim21-pkgs/packages/bencode2>.
76
+
77
+ ## basic usage
78
+
79
+ ```python
80
+ import bencode2
81
+
82
+ assert bencode2.bdecode(b"d4:spaml1:a1:bee") == {b"spam": [b"a", b"b"]}
83
+
84
+ assert bencode2.bencode({'hello': 'world'}) == b'd5:hello5:worlde'
85
+ ```
86
+
87
+ ### Decoding
88
+
89
+ | bencode type | python type |
90
+ | :----------: | :---------: |
91
+ | integer | `int` |
92
+ | string | `bytes` |
93
+ | array | `list` |
94
+ | dictionary | `dict` |
95
+
96
+ bencode have 4 native types, integer, string, array and dictionary.
97
+
98
+ This package will decode integer to `int`, array to `list` and
99
+ dictionary to `dict`.
100
+
101
+ Because bencode string is not defined as utf-8 string, and will contain raw bytes
102
+ bencode2 will decode bencode string to python `bytes`.
103
+
104
+ ### Encoding
105
+
106
+ | python type | bencode type |
107
+ | :-------------------------------: | :----------: |
108
+ | `bool` | integer 0/1 |
109
+ | `int`, `enum.IntEnum` | integer |
110
+ | `str`, `enum.StrEnum` | string |
111
+ | `bytes`, `bytearray`,`memoryview` | string |
112
+ | `list`, `tuple`, `NamedTuple` | array |
113
+ | `dict`, `OrderedDict` | dictionary |
114
+ | `types.MaapingProxy` | dictionary |
115
+ | dataclasses | dictionary |
116
+
117
+ ## free threading
118
+
119
+ bencode2 have a free threading wheel on pypi, build with GIL disabled.
120
+
121
+ When encoding or decoding, it will not acquire GIL and may call non-thread-safy c-api,
122
+ which mean it's the caller's responsibility to ensure thread safety.
123
+
124
+ When calling `bencode`, it's safe to encode same object in multiple threading,
125
+ but it's not safe to encoding a object and change it in another thread at same time.
126
+
127
+ Also, when decoding, `bytes` objects are immutable so it's safe to be used in multiple
128
+ threading,
129
+ but `memoryview` and `bytearray` maybe not, please make sure underlay data doesn't
130
+ change when decoding.
131
+
132
+ ## Development
133
+
134
+ This project use [meson](https://github.com/mesonbuild/meson) for building.
135
+
136
+ For testing pure python library,
137
+ make sure all so/pyd files in `src/bencode2` are removed, then run
138
+ `PYTHONPATH=src pytest --assert-pkg-compiled=false`.
139
+
140
+ For testing native extension, meson-python doesn't provide same function with
141
+ `python setup.py build_ext --inplace`.
142
+
143
+ So you will need to run command like this:
144
+
145
+ ```shell
146
+ meson setup build
147
+ meson compile -C build
148
+ ninja -C build copy
149
+ ```
150
+
151
+ ninja will need to build so/pyd with meson and copy it to `src/bencode2`,
152
+
153
+ then run tests with `PYTHONPATH=src pytest --assert-pkg-compiled=true`.
@@ -0,0 +1,9 @@
1
+ bencode2-0.3.27.dist-info/METADATA,sha256=i3yZybb2F0ApYlRoPZw9lpuShFgNbByPTz7Xoko9PWM,5283
2
+ bencode2-0.3.27.dist-info/WHEEL,sha256=HxTT_sQ360-hdmVNeweyxE6Ly6SvGGdMmSsv6PXi_1M,85
3
+ bencode2/__bencode.cp313-win_arm64.pyd,sha256=WeFAIO2gGRAy5dlebXgqWbq3EmxwPv8_Cek70AiysQo,191488
4
+ bencode2/__init__.py,sha256=z2lFW5z2l6pTccyzUGXkfFEcxqYwVgZ-iRuqrYC70bE,400
5
+ bencode2/__init__.pyi,sha256=9IRJDThFEll3bdqQTH56r9y0bpI-D3qN5VM1LIRpX04,245
6
+ bencode2/__encoder.py,sha256=ISaM-PLNdv8BKQFTq8YC_28czys9a0PbxzykCESYrKo,4355
7
+ bencode2/__decoder.py,sha256=drQInAg7UdKhfJgXI-40r3tPUvS20ep65lmcAGkGp9M,6398
8
+ bencode2/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ bencode2-0.3.27.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: meson
3
+ Root-Is-Purelib: false
4
+ Tag: cp313-cp313-win_arm64