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 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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,9 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/binsl/__init__.py
5
+ src/binsl/binsl.py
6
+ src/binsl.egg-info/PKG-INFO
7
+ src/binsl.egg-info/SOURCES.txt
8
+ src/binsl.egg-info/dependency_links.txt
9
+ src/binsl.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ binsl