binsl 0.1__tar.gz
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.
- binsl-0.1/LICENSE +21 -0
- binsl-0.1/PKG-INFO +34 -0
- binsl-0.1/README.md +1 -0
- binsl-0.1/pyproject.toml +25 -0
- binsl-0.1/setup.cfg +4 -0
- binsl-0.1/src/binsl/__init__.py +1 -0
- binsl-0.1/src/binsl/binsl.py +743 -0
- binsl-0.1/src/binsl.egg-info/PKG-INFO +34 -0
- binsl-0.1/src/binsl.egg-info/SOURCES.txt +9 -0
- binsl-0.1/src/binsl.egg-info/dependency_links.txt +1 -0
- binsl-0.1/src/binsl.egg-info/top_level.txt +1 -0
binsl-0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 stas96111
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
binsl-0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: binsl
|
|
3
|
+
Version: 0.1
|
|
4
|
+
Summary: Small binary reader/writer library for Python.
|
|
5
|
+
Author-email: stas96111 <stas96111@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 stas96111
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/stas96111/binsl
|
|
29
|
+
Classifier: Programming Language :: Python :: 3
|
|
30
|
+
Classifier: Operating System :: OS Independent
|
|
31
|
+
Requires-Python: >=3.8
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
Small binary reader/writer library for Python.
|
binsl-0.1/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Small binary reader/writer library for Python.
|
binsl-0.1/pyproject.toml
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "binsl"
|
|
7
|
+
version = "0.1"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "stas96111", email = "stas96111@gmail.com" },
|
|
10
|
+
]
|
|
11
|
+
description = "Small binary reader/writer library for Python."
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.8"
|
|
14
|
+
dependencies = []
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
license = {file = "LICENSE"}
|
|
20
|
+
|
|
21
|
+
[tool.setuptools]
|
|
22
|
+
license-files = []
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://github.com/stas96111/binsl"
|
binsl-0.1/setup.cfg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .binsl import BinReader, BinWriter, Position, Endian
|
|
@@ -0,0 +1,743 @@
|
|
|
1
|
+
import mmap
|
|
2
|
+
import struct
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
from enum import IntEnum, StrEnum
|
|
5
|
+
from io import SEEK_CUR, SEEK_END, SEEK_SET
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
HEX_TABLE = [f"{i:02X}" for i in range(256)]
|
|
9
|
+
|
|
10
|
+
ASCII_TABLE = [
|
|
11
|
+
chr(i) if 32 <= i <= 126 else "."
|
|
12
|
+
for i in range(256)
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
_STRUCTS = {
|
|
16
|
+
"<": {
|
|
17
|
+
"u8": struct.Struct("<B"),
|
|
18
|
+
"u16": struct.Struct("<H"),
|
|
19
|
+
"u32": struct.Struct("<I"),
|
|
20
|
+
"u64": struct.Struct("<Q"),
|
|
21
|
+
"i8": struct.Struct("<b"),
|
|
22
|
+
"i16": struct.Struct("<h"),
|
|
23
|
+
"i32": struct.Struct("<i"),
|
|
24
|
+
"i64": struct.Struct("<q"),
|
|
25
|
+
"f": struct.Struct("<f"),
|
|
26
|
+
"d": struct.Struct("<d"),
|
|
27
|
+
},
|
|
28
|
+
">": {
|
|
29
|
+
"u8": struct.Struct(">B"),
|
|
30
|
+
"u16": struct.Struct(">H"),
|
|
31
|
+
"u32": struct.Struct(">I"),
|
|
32
|
+
"u64": struct.Struct(">Q"),
|
|
33
|
+
"i8": struct.Struct(">b"),
|
|
34
|
+
"i16": struct.Struct(">h"),
|
|
35
|
+
"i32": struct.Struct(">i"),
|
|
36
|
+
"i64": struct.Struct(">q"),
|
|
37
|
+
"f": struct.Struct(">f"),
|
|
38
|
+
"d": struct.Struct(">d"),
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class Position(IntEnum):
|
|
43
|
+
CURRENT = SEEK_CUR
|
|
44
|
+
END = SEEK_END
|
|
45
|
+
SET = SEEK_SET
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Endian(StrEnum):
|
|
49
|
+
BIG = ">"
|
|
50
|
+
LITTLE = "<"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class BinReader:
|
|
54
|
+
"""Create binary reader.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
file: Path | str | bytes | None
|
|
58
|
+
endian: Endian
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
reader = BinReader()
|
|
62
|
+
|
|
63
|
+
with BinReader() as reader:
|
|
64
|
+
pass
|
|
65
|
+
```
|
|
66
|
+
"""
|
|
67
|
+
__slots__ = (
|
|
68
|
+
"path",
|
|
69
|
+
"size",
|
|
70
|
+
"pos",
|
|
71
|
+
"endian",
|
|
72
|
+
"_mm",
|
|
73
|
+
"_view",
|
|
74
|
+
"_file_obj",
|
|
75
|
+
"_u8",
|
|
76
|
+
"_u16",
|
|
77
|
+
"_u32",
|
|
78
|
+
"_u64",
|
|
79
|
+
"_i8",
|
|
80
|
+
"_i16",
|
|
81
|
+
"_i32",
|
|
82
|
+
"_i64",
|
|
83
|
+
"_f",
|
|
84
|
+
"_d",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def __init__(self, file: Path | str | bytes = b"", endian: Endian = Endian.LITTLE) -> BinReader:
|
|
88
|
+
self.path = None
|
|
89
|
+
self.size = 0
|
|
90
|
+
self.pos = 0
|
|
91
|
+
self.set_endian(endian)
|
|
92
|
+
|
|
93
|
+
self._mm = None
|
|
94
|
+
self._view = None
|
|
95
|
+
self._file_obj = None
|
|
96
|
+
|
|
97
|
+
if isinstance(file, (str, Path)):
|
|
98
|
+
self.path = str(file)
|
|
99
|
+
self._file_obj = open(file, "rb")
|
|
100
|
+
self._mm = mmap.mmap(self._file_obj.fileno(), 0, access=mmap.ACCESS_READ)
|
|
101
|
+
self._view = memoryview(self._mm)
|
|
102
|
+
self.size = len(self._mm)
|
|
103
|
+
elif isinstance(file, (bytes, bytearray)):
|
|
104
|
+
self._mm = None # IMPORTANT
|
|
105
|
+
self._view = memoryview(file)
|
|
106
|
+
self.size = len(file)
|
|
107
|
+
else:
|
|
108
|
+
raise ValueError("File must be: str(path), bytes, or bytearray.")
|
|
109
|
+
|
|
110
|
+
def set_endian(self, endian: Endian = Endian.LITTLE):
|
|
111
|
+
"""Change endines of the reader.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
endian: Endian
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
reader.set_endian(Endian.Big)
|
|
118
|
+
```
|
|
119
|
+
"""
|
|
120
|
+
self.endian = endian
|
|
121
|
+
s = _STRUCTS[endian]
|
|
122
|
+
self._u8 = s["u8"]
|
|
123
|
+
self._u16 = s["u16"]
|
|
124
|
+
self._u32 = s["u32"]
|
|
125
|
+
self._u64 = s["u64"]
|
|
126
|
+
self._i8 = s["i8"]
|
|
127
|
+
self._i16 = s["i16"]
|
|
128
|
+
self._i32 = s["i32"]
|
|
129
|
+
self._i64 = s["i64"]
|
|
130
|
+
self._f = s["f"]
|
|
131
|
+
self._d = s["d"]
|
|
132
|
+
|
|
133
|
+
def _check(self, size):
|
|
134
|
+
if self.pos + size > self.size:
|
|
135
|
+
raise EOFError("Read beyond end")
|
|
136
|
+
|
|
137
|
+
def get_pos(self):
|
|
138
|
+
"""Get cursor possition of the cursor.
|
|
139
|
+
"""
|
|
140
|
+
return self.pos
|
|
141
|
+
|
|
142
|
+
def set_pos(self, position: int, where=Position.SET):
|
|
143
|
+
"""Set cursor possition of the cursor.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
position: int
|
|
147
|
+
where: Possition.
|
|
148
|
+
"""
|
|
149
|
+
if where == Position.CURRENT:
|
|
150
|
+
self.pos += position
|
|
151
|
+
elif where == Position.END:
|
|
152
|
+
self.pos = self.size - abs(position)
|
|
153
|
+
elif where == Position.SET:
|
|
154
|
+
self.pos = position
|
|
155
|
+
|
|
156
|
+
if not (0 <= self.pos <= self.size):
|
|
157
|
+
raise ValueError("Position out of bounds.")
|
|
158
|
+
|
|
159
|
+
def skip(self, offset: int):
|
|
160
|
+
"""Move the current position forward by a given offset.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
offset: Number of bytes to skip from the current position.
|
|
164
|
+
"""
|
|
165
|
+
self.set_pos(offset, SEEK_CUR)
|
|
166
|
+
|
|
167
|
+
def get_size(self):
|
|
168
|
+
return self.size
|
|
169
|
+
|
|
170
|
+
def align(self, alignment):
|
|
171
|
+
self.pos = (self.pos + alignment - 1) & ~(alignment - 1)
|
|
172
|
+
if self.pos > self.size:
|
|
173
|
+
raise EOFError("Alignment beyond EOF")
|
|
174
|
+
|
|
175
|
+
def read(self, size: int) -> bytes:
|
|
176
|
+
"""Read a number of bytes from the current position.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
size: Number of bytes to read.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
The bytes that were read.
|
|
183
|
+
"""
|
|
184
|
+
pos = self.pos
|
|
185
|
+
end = pos + size
|
|
186
|
+
if end > self.size:
|
|
187
|
+
raise EOFError(f"Attempt to read {size} beyond the end of the file.")
|
|
188
|
+
data = self._view[pos:end]
|
|
189
|
+
self.pos = end
|
|
190
|
+
return data.tobytes()
|
|
191
|
+
|
|
192
|
+
def read_int(self, size: int, signed: bool = False, byteorder="little") -> int:
|
|
193
|
+
"""Read an integer from the current position.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
size: Number of bytes to read.
|
|
197
|
+
signed: Whether the integer is signed.
|
|
198
|
+
byteorder: Byte order ('little' or 'big').
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
The integer value decoded from the bytes.
|
|
202
|
+
"""
|
|
203
|
+
val = self._view[self.pos : self.pos + size]
|
|
204
|
+
self.pos += size
|
|
205
|
+
return int.from_bytes(val, byteorder=byteorder, signed=signed)
|
|
206
|
+
|
|
207
|
+
def uint8(self) -> int:
|
|
208
|
+
"""Read an unsigned 8-bit integer."""
|
|
209
|
+
self._check(1)
|
|
210
|
+
val = self._u8.unpack_from(self._view, self.pos)[0]
|
|
211
|
+
self.pos += 1
|
|
212
|
+
return val
|
|
213
|
+
|
|
214
|
+
def uint16(self) -> int:
|
|
215
|
+
"""Read an unsigned 16-bit integer."""
|
|
216
|
+
self._check(2)
|
|
217
|
+
val = self._u16.unpack_from(self._view, self.pos)[0]
|
|
218
|
+
self.pos += 2
|
|
219
|
+
return val
|
|
220
|
+
|
|
221
|
+
def uint32(self) -> int:
|
|
222
|
+
"""Read an unsigned 32-bit integer."""
|
|
223
|
+
self._check(4)
|
|
224
|
+
val = self._u32.unpack_from(self._view, self.pos)[0]
|
|
225
|
+
self.pos += 4
|
|
226
|
+
return val
|
|
227
|
+
|
|
228
|
+
def uint64(self) -> int:
|
|
229
|
+
"""Read an unsigned 64-bit integer."""
|
|
230
|
+
self._check(8)
|
|
231
|
+
val = self._u64.unpack_from(self._view, self.pos)[0]
|
|
232
|
+
self.pos += 8
|
|
233
|
+
return val
|
|
234
|
+
|
|
235
|
+
def uint128(self) -> int:
|
|
236
|
+
"""Read an unsigned 128-bit integer."""
|
|
237
|
+
self._check(16)
|
|
238
|
+
val = int.from_bytes(
|
|
239
|
+
self._view[self.pos : self.pos + 16],
|
|
240
|
+
byteorder="little" if self.endian == "<" else "big",
|
|
241
|
+
signed=False,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
self.pos += 16
|
|
245
|
+
return val
|
|
246
|
+
|
|
247
|
+
def int8(self) -> int:
|
|
248
|
+
"""Read a signed 8-bit integer."""
|
|
249
|
+
self._check(1)
|
|
250
|
+
val = self._i8.unpack_from(self._view, self.pos)[0]
|
|
251
|
+
self.pos += 1
|
|
252
|
+
return val
|
|
253
|
+
|
|
254
|
+
def int16(self) -> int:
|
|
255
|
+
"""Read a signed 16-bit integer."""
|
|
256
|
+
self._check(2)
|
|
257
|
+
val = self._i16.unpack_from(self._view, self.pos)[0]
|
|
258
|
+
self.pos += 2
|
|
259
|
+
return val
|
|
260
|
+
|
|
261
|
+
def int32(self) -> int:
|
|
262
|
+
"""Read a signed 32-bit integer."""
|
|
263
|
+
self._check(4)
|
|
264
|
+
val = self._i32.unpack_from(self._view, self.pos)[0]
|
|
265
|
+
self.pos += 4
|
|
266
|
+
return val
|
|
267
|
+
|
|
268
|
+
def int64(self) -> int:
|
|
269
|
+
"""Read a signed 64-bit integer."""
|
|
270
|
+
self._check(8)
|
|
271
|
+
val = self._i64.unpack_from(self._view, self.pos)[0]
|
|
272
|
+
self.pos += 8
|
|
273
|
+
return val
|
|
274
|
+
|
|
275
|
+
def int128(self) -> int:
|
|
276
|
+
"""Read a signed 128-bit integer."""
|
|
277
|
+
self._check(16)
|
|
278
|
+
val = int.from_bytes(
|
|
279
|
+
self._view[self.pos : self.pos + 16],
|
|
280
|
+
byteorder="little" if self.endian == "<" else "big",
|
|
281
|
+
signed=True,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
self.pos += 16
|
|
285
|
+
return val
|
|
286
|
+
|
|
287
|
+
def float(self) -> float:
|
|
288
|
+
"""Read an 32-bit float value."""
|
|
289
|
+
self._check(4)
|
|
290
|
+
val = self._f.unpack_from(self._view, self.pos)[0]
|
|
291
|
+
self.pos += 4
|
|
292
|
+
return val
|
|
293
|
+
|
|
294
|
+
def double(self) -> float:
|
|
295
|
+
"""Read an 64-bit float value."""
|
|
296
|
+
self._check(8)
|
|
297
|
+
val = self._d.unpack_from(self._view, self.pos)[0]
|
|
298
|
+
self.pos += 8
|
|
299
|
+
return val
|
|
300
|
+
|
|
301
|
+
def bool(self) -> bool:
|
|
302
|
+
"""Read a boolean value."""
|
|
303
|
+
val = self.uint8()
|
|
304
|
+
return val != 0
|
|
305
|
+
|
|
306
|
+
def string(self, encoding="utf-8", size=None) -> str:
|
|
307
|
+
"""Read a string.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
encoding: Text encoding used to decode the bytes.
|
|
311
|
+
length: Number of bytes to read.
|
|
312
|
+
"""
|
|
313
|
+
if size is None:
|
|
314
|
+
size = self.uint32()
|
|
315
|
+
|
|
316
|
+
end = self.pos + size
|
|
317
|
+
|
|
318
|
+
if end > self.size:
|
|
319
|
+
raise EOFError("String larger than the file size.")
|
|
320
|
+
|
|
321
|
+
data = self._view[self.pos:end].tobytes()
|
|
322
|
+
self.pos = end
|
|
323
|
+
|
|
324
|
+
return data.decode(encoding)
|
|
325
|
+
|
|
326
|
+
def cstring(self, encoding="utf-8") -> str:
|
|
327
|
+
"""Read a C-like string.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
encoding: Text encoding used to decode the bytes.
|
|
331
|
+
length: Number of bytes to read.
|
|
332
|
+
"""
|
|
333
|
+
start = self.pos
|
|
334
|
+
mv = self._view
|
|
335
|
+
size = self.size
|
|
336
|
+
|
|
337
|
+
while self.pos < size:
|
|
338
|
+
if mv[self.pos] == 0:
|
|
339
|
+
break
|
|
340
|
+
self.pos += 1
|
|
341
|
+
else:
|
|
342
|
+
raise EOFError("Unterminated C-string")
|
|
343
|
+
|
|
344
|
+
data = mv[start:self.pos].tobytes()
|
|
345
|
+
self.pos += 1
|
|
346
|
+
|
|
347
|
+
return data.decode(encoding)
|
|
348
|
+
|
|
349
|
+
def list(self, func, length=None):
|
|
350
|
+
"""Read a list of items using a parser function.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
func: Function that reads a single element from reader.
|
|
354
|
+
length: Number of items to read.
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
List of parsed items.
|
|
358
|
+
"""
|
|
359
|
+
|
|
360
|
+
if length is None:
|
|
361
|
+
length = self.uint32()
|
|
362
|
+
return [func(self) for _ in range(length)]
|
|
363
|
+
|
|
364
|
+
def buffer(self, offset, size):
|
|
365
|
+
"""Create a sub-reader from a slice of the buffer.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
offset: Start position of the slice.
|
|
369
|
+
size: Number of bytes to include.
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
A new BinReader instance.
|
|
373
|
+
|
|
374
|
+
```python
|
|
375
|
+
with reader.buffer(size=200) as buffer:
|
|
376
|
+
value = buffer.uint8()
|
|
377
|
+
```
|
|
378
|
+
"""
|
|
379
|
+
|
|
380
|
+
if offset is None:
|
|
381
|
+
offset = self.pos
|
|
382
|
+
|
|
383
|
+
if offset < 0 or offset + size > self.size:
|
|
384
|
+
raise EOFError("Buffer out of bounds")
|
|
385
|
+
|
|
386
|
+
data = self._view[offset: offset + size]
|
|
387
|
+
return BinReader(data, endian=self.endian)
|
|
388
|
+
|
|
389
|
+
def remain(self, size: int):
|
|
390
|
+
return self.size - self.pos >= size
|
|
391
|
+
|
|
392
|
+
def hexdump(self, offset = None, size = None, width = 16, height = None, group = 8):
|
|
393
|
+
"""Return hex dump of file."""
|
|
394
|
+
if offset is None:
|
|
395
|
+
offset = self.pos
|
|
396
|
+
|
|
397
|
+
if height is not None:
|
|
398
|
+
size = width * height
|
|
399
|
+
|
|
400
|
+
if size is None:
|
|
401
|
+
size = min(256, self.size - offset)
|
|
402
|
+
|
|
403
|
+
end = min(offset + size, self.size)
|
|
404
|
+
|
|
405
|
+
lines = []
|
|
406
|
+
|
|
407
|
+
header_parts = []
|
|
408
|
+
|
|
409
|
+
for i in range(width):
|
|
410
|
+
if i and i % group == 0:
|
|
411
|
+
header_parts.append(" ")
|
|
412
|
+
|
|
413
|
+
header_parts.append(HEX_TABLE[i])
|
|
414
|
+
|
|
415
|
+
lines.append("Hex Dump " + " ".join(header_parts))
|
|
416
|
+
lines.append("")
|
|
417
|
+
|
|
418
|
+
view = self._view
|
|
419
|
+
|
|
420
|
+
for row in range(offset, end, width):
|
|
421
|
+
chunk = view[row:min(row + width, end)]
|
|
422
|
+
|
|
423
|
+
hex_parts = []
|
|
424
|
+
ascii_parts = []
|
|
425
|
+
|
|
426
|
+
for i, b in enumerate(chunk):
|
|
427
|
+
if i and i % group == 0:
|
|
428
|
+
hex_parts.append(" ")
|
|
429
|
+
|
|
430
|
+
hex_parts.append(HEX_TABLE[b])
|
|
431
|
+
ascii_parts.append(ASCII_TABLE[b])
|
|
432
|
+
|
|
433
|
+
hex_text = " ".join(hex_parts)
|
|
434
|
+
|
|
435
|
+
expected_width = width * 3 + ((width - 1) // group)
|
|
436
|
+
hex_text = hex_text.ljust(expected_width)
|
|
437
|
+
|
|
438
|
+
ascii_text = "".join(ascii_parts)
|
|
439
|
+
|
|
440
|
+
lines.append(
|
|
441
|
+
f"{row:08X} {hex_text} {ascii_text}"
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
return "\n".join(lines)
|
|
445
|
+
|
|
446
|
+
@contextmanager
|
|
447
|
+
def at(self, offset):
|
|
448
|
+
"""Temporarily move the read position to an offset.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
offset: Position to temporarily move to.
|
|
452
|
+
|
|
453
|
+
```python
|
|
454
|
+
with reader.at(offset) as temp_reader:
|
|
455
|
+
value = temp_reader.uint8()
|
|
456
|
+
```
|
|
457
|
+
"""
|
|
458
|
+
old = self.pos
|
|
459
|
+
self.pos = offset
|
|
460
|
+
try:
|
|
461
|
+
yield self
|
|
462
|
+
finally:
|
|
463
|
+
self.pos = old
|
|
464
|
+
|
|
465
|
+
def close(self):
|
|
466
|
+
if self._view is not None:
|
|
467
|
+
self._view.release()
|
|
468
|
+
self._view = None
|
|
469
|
+
|
|
470
|
+
if self._mm is not None:
|
|
471
|
+
self._mm.close()
|
|
472
|
+
self._mm = None
|
|
473
|
+
|
|
474
|
+
if self._file_obj is not None:
|
|
475
|
+
self._file_obj.close()
|
|
476
|
+
self._file_obj = None
|
|
477
|
+
|
|
478
|
+
def __enter__(self):
|
|
479
|
+
return self
|
|
480
|
+
|
|
481
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
482
|
+
self.close()
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
class BinWriter:
|
|
486
|
+
__slots__ = (
|
|
487
|
+
"path",
|
|
488
|
+
"size",
|
|
489
|
+
"pos",
|
|
490
|
+
"endian",
|
|
491
|
+
"_mm",
|
|
492
|
+
"_view",
|
|
493
|
+
"_file_obj",
|
|
494
|
+
"_buf",
|
|
495
|
+
"_u8",
|
|
496
|
+
"_u16",
|
|
497
|
+
"_u32",
|
|
498
|
+
"_u64",
|
|
499
|
+
"_i8",
|
|
500
|
+
"_i16",
|
|
501
|
+
"_i32",
|
|
502
|
+
"_i64",
|
|
503
|
+
"_f",
|
|
504
|
+
"_d",
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
def __init__(
|
|
508
|
+
self, file: str | Path | bytes | bytearray | None = None, endian: Endian = Endian.LITTLE, initial_size=1024, append=False
|
|
509
|
+
):
|
|
510
|
+
"""
|
|
511
|
+
file:
|
|
512
|
+
- str -> file path (mmap-backed)
|
|
513
|
+
- None -> in-memory (bytearray)
|
|
514
|
+
"""
|
|
515
|
+
self.path = None
|
|
516
|
+
self.pos = 0
|
|
517
|
+
self.size = 0
|
|
518
|
+
self._mm = None
|
|
519
|
+
self._view = None
|
|
520
|
+
self._file_obj = None
|
|
521
|
+
self._buf = None
|
|
522
|
+
|
|
523
|
+
self.set_endian(endian)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
if isinstance(file, (str, Path)):
|
|
527
|
+
exists = Path(file).exists()
|
|
528
|
+
if append and exists:
|
|
529
|
+
self._file_obj = open(file, "r+b")
|
|
530
|
+
self._file_obj.seek(0, SEEK_END)
|
|
531
|
+
existing = self._file_obj.tell()
|
|
532
|
+
map_size = max(existing, initial_size)
|
|
533
|
+
if existing < map_size:
|
|
534
|
+
self._file_obj.write(b"\x00" * (map_size - existing))
|
|
535
|
+
self._file_obj.flush()
|
|
536
|
+
self.pos = existing
|
|
537
|
+
else:
|
|
538
|
+
self._file_obj = open(file, "w+b")
|
|
539
|
+
self._file_obj.write(b"\x00" * initial_size)
|
|
540
|
+
self._file_obj.flush()
|
|
541
|
+
map_size = initial_size
|
|
542
|
+
self.pos = 0
|
|
543
|
+
self.size = map_size
|
|
544
|
+
self._mm = mmap.mmap(self._file_obj.fileno(), map_size)
|
|
545
|
+
self._view = memoryview(self._mm)
|
|
546
|
+
|
|
547
|
+
elif isinstance(file, (bytes, bytearray)):
|
|
548
|
+
self._buf = bytearray(file) # copy into a mutable bytearray
|
|
549
|
+
self._view = memoryview(self._buf)
|
|
550
|
+
self.size = len(self._buf)
|
|
551
|
+
self.pos = len(self._buf)
|
|
552
|
+
|
|
553
|
+
elif file is None:
|
|
554
|
+
self._buf = bytearray(initial_size)
|
|
555
|
+
self._view = memoryview(self._buf)
|
|
556
|
+
self.size = initial_size
|
|
557
|
+
|
|
558
|
+
else:
|
|
559
|
+
raise ValueError("Writer file must be: str(path) or None")
|
|
560
|
+
|
|
561
|
+
def _ensure(self, needed: int):
|
|
562
|
+
if self.pos + needed <= self.size:
|
|
563
|
+
return
|
|
564
|
+
|
|
565
|
+
new_size = max(self.size * 2, self.pos + needed)
|
|
566
|
+
|
|
567
|
+
if self._mm is not None:
|
|
568
|
+
if self._view is not None:
|
|
569
|
+
self._view.release()
|
|
570
|
+
self._view = None
|
|
571
|
+
|
|
572
|
+
self._mm.resize(new_size)
|
|
573
|
+
self._view = memoryview(self._mm)
|
|
574
|
+
|
|
575
|
+
else:
|
|
576
|
+
# Release memoryview before resizing bytearray
|
|
577
|
+
if self._view is not None:
|
|
578
|
+
self._view.release()
|
|
579
|
+
self._view = None
|
|
580
|
+
|
|
581
|
+
self._buf.extend(b"\x00" * (new_size - self.size))
|
|
582
|
+
self._view = memoryview(self._buf)
|
|
583
|
+
|
|
584
|
+
self.size = new_size
|
|
585
|
+
|
|
586
|
+
def set_endian(self, endian: Endian):
|
|
587
|
+
self.endian = endian
|
|
588
|
+
s = _STRUCTS[endian]
|
|
589
|
+
self._u8 = s["u8"]
|
|
590
|
+
self._u16 = s["u16"]
|
|
591
|
+
self._u32 = s["u32"]
|
|
592
|
+
self._u64 = s["u64"]
|
|
593
|
+
self._i8 = s["i8"]
|
|
594
|
+
self._i16 = s["i16"]
|
|
595
|
+
self._i32 = s["i32"]
|
|
596
|
+
self._i64 = s["i64"]
|
|
597
|
+
self._f = s["f"]
|
|
598
|
+
self._d = s["d"]
|
|
599
|
+
|
|
600
|
+
def get_pos(self):
|
|
601
|
+
return self.pos
|
|
602
|
+
|
|
603
|
+
def set_pos(self, position: int, where=Position.CURRENT):
|
|
604
|
+
if where == Position.SET:
|
|
605
|
+
self.pos = position
|
|
606
|
+
elif where == Position.CURRENT:
|
|
607
|
+
self.pos += position
|
|
608
|
+
elif where == Position.END:
|
|
609
|
+
self.pos = self.size - abs(position)
|
|
610
|
+
else:
|
|
611
|
+
raise ValueError("Invalid seek mode")
|
|
612
|
+
|
|
613
|
+
def skip(self, offset: int):
|
|
614
|
+
self.pos += offset
|
|
615
|
+
|
|
616
|
+
def write(self, data: bytes | bytearray | memoryview):
|
|
617
|
+
n = len(data)
|
|
618
|
+
self._ensure(n)
|
|
619
|
+
self._view[self.pos : self.pos + n] = data
|
|
620
|
+
self.pos += n
|
|
621
|
+
|
|
622
|
+
def uint8(self, v: int):
|
|
623
|
+
self._ensure(1)
|
|
624
|
+
self._u8.pack_into(self._view, self.pos, v)
|
|
625
|
+
self.pos += 1
|
|
626
|
+
|
|
627
|
+
def uint16(self, v: int):
|
|
628
|
+
self._ensure(2)
|
|
629
|
+
self._u16.pack_into(self._view, self.pos, v)
|
|
630
|
+
self.pos += 2
|
|
631
|
+
|
|
632
|
+
def uint32(self, v: int):
|
|
633
|
+
self._ensure(4)
|
|
634
|
+
self._u32.pack_into(self._view, self.pos, v)
|
|
635
|
+
self.pos += 4
|
|
636
|
+
|
|
637
|
+
def uint64(self, v: int):
|
|
638
|
+
self._ensure(8)
|
|
639
|
+
self._u64.pack_into(self._view, self.pos, v)
|
|
640
|
+
self.pos += 8
|
|
641
|
+
|
|
642
|
+
def uint128(self, v: int):
|
|
643
|
+
self._ensure(16)
|
|
644
|
+
self._view[self.pos : self.pos + 16] = v.to_bytes(
|
|
645
|
+
16, "little" if self.endian == "<" else "big", signed=False
|
|
646
|
+
)
|
|
647
|
+
self.pos += 16
|
|
648
|
+
|
|
649
|
+
def int8(self, v: int):
|
|
650
|
+
self._ensure(1)
|
|
651
|
+
self._i8.pack_into(self._view, self.pos, v)
|
|
652
|
+
self.pos += 1
|
|
653
|
+
|
|
654
|
+
def int16(self, v: int):
|
|
655
|
+
self._ensure(2)
|
|
656
|
+
self._i16.pack_into(self._view, self.pos, v)
|
|
657
|
+
self.pos += 2
|
|
658
|
+
|
|
659
|
+
def int32(self, v: int):
|
|
660
|
+
self._ensure(4)
|
|
661
|
+
self._i32.pack_into(self._view, self.pos, v)
|
|
662
|
+
self.pos += 4
|
|
663
|
+
|
|
664
|
+
def int64(self, v: int):
|
|
665
|
+
self._ensure(8)
|
|
666
|
+
self._i64.pack_into(self._view, self.pos, v)
|
|
667
|
+
self.pos += 8
|
|
668
|
+
|
|
669
|
+
def int128(self, v: int):
|
|
670
|
+
self._ensure(16)
|
|
671
|
+
self._view[self.pos : self.pos + 16] = v.to_bytes(
|
|
672
|
+
16, "little" if self.endian == "<" else "big", signed=True
|
|
673
|
+
)
|
|
674
|
+
self.pos += 16
|
|
675
|
+
|
|
676
|
+
def float(self, v: float):
|
|
677
|
+
self._ensure(4)
|
|
678
|
+
self._f.pack_into(self._view, self.pos, v)
|
|
679
|
+
self.pos += 4
|
|
680
|
+
|
|
681
|
+
def double(self, v: float):
|
|
682
|
+
self._ensure(8)
|
|
683
|
+
self._d.pack_into(self._view, self.pos, v)
|
|
684
|
+
self.pos += 8
|
|
685
|
+
|
|
686
|
+
def bool(self, v: bool):
|
|
687
|
+
self.uint8(1 if v else 0)
|
|
688
|
+
|
|
689
|
+
def get_bytes(self) -> bytes:
|
|
690
|
+
if self._mm is not None:
|
|
691
|
+
return self._mm[:self.pos]
|
|
692
|
+
|
|
693
|
+
if self._buf is not None:
|
|
694
|
+
return bytes(self._buf[:self.pos])
|
|
695
|
+
|
|
696
|
+
return b""
|
|
697
|
+
|
|
698
|
+
def string(self, value: str, encoding="utf-8", length=None):
|
|
699
|
+
data = value.encode(encoding)
|
|
700
|
+
if length is None:
|
|
701
|
+
length = len(data)
|
|
702
|
+
self.uint32(length)
|
|
703
|
+
self.write(data)
|
|
704
|
+
|
|
705
|
+
def cstring(self, value: str, encoding="utf-8"):
|
|
706
|
+
data = value.encode(encoding) + b"\x00"
|
|
707
|
+
self.write(data)
|
|
708
|
+
|
|
709
|
+
def list(self, items, write_func, length=None):
|
|
710
|
+
if length is None:
|
|
711
|
+
self.uint32(len(items))
|
|
712
|
+
for item in items:
|
|
713
|
+
write_func(item)
|
|
714
|
+
|
|
715
|
+
@contextmanager
|
|
716
|
+
def at(self, offset):
|
|
717
|
+
old = self.pos
|
|
718
|
+
self.pos = offset
|
|
719
|
+
try:
|
|
720
|
+
yield self
|
|
721
|
+
finally:
|
|
722
|
+
self.pos = old
|
|
723
|
+
|
|
724
|
+
def close(self):
|
|
725
|
+
if self._view is not None:
|
|
726
|
+
self._view.release()
|
|
727
|
+
self._view = None
|
|
728
|
+
|
|
729
|
+
if self._mm is not None:
|
|
730
|
+
self._mm.flush()
|
|
731
|
+
self._mm.close()
|
|
732
|
+
self._mm = None
|
|
733
|
+
|
|
734
|
+
if self._file_obj is not None:
|
|
735
|
+
self._file_obj.truncate(self.pos)
|
|
736
|
+
self._file_obj.close()
|
|
737
|
+
self._file_obj = None
|
|
738
|
+
|
|
739
|
+
def __enter__(self):
|
|
740
|
+
return self
|
|
741
|
+
|
|
742
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
743
|
+
self.close()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: binsl
|
|
3
|
+
Version: 0.1
|
|
4
|
+
Summary: Small binary reader/writer library for Python.
|
|
5
|
+
Author-email: stas96111 <stas96111@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 stas96111
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/stas96111/binsl
|
|
29
|
+
Classifier: Programming Language :: Python :: 3
|
|
30
|
+
Classifier: Operating System :: OS Independent
|
|
31
|
+
Requires-Python: >=3.8
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
Small binary reader/writer library for Python.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
binsl
|