mscs 2.2.1__tar.gz → 2.3.0__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.
mscs-2.3.0/PKG-INFO ADDED
@@ -0,0 +1,315 @@
1
+ Metadata-Version: 2.4
2
+ Name: mscs
3
+ Version: 2.3.0
4
+ Summary: Safe, fast serialization for Python — a secure replacement for pickle with HMAC authentication, native numpy/PyTorch support.
5
+ Project-URL: Homepage, https://github.com/ElEscribanoSilente/MSC-Serial
6
+ Project-URL: Repository, https://github.com/ElEscribanoSilente/MSC-Serial
7
+ Project-URL: Issues, https://github.com/ElEscribanoSilente/MSC-Serial/issues
8
+ Project-URL: Changelog, https://github.com/ElEscribanoSilente/MSC-Serial/blob/main/CHANGELOG.md
9
+ Author: Esraderey
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: binary,checkpoint,fast,hmac,integrity,numpy,pickle,pytorch,safe,secure,serialization,tensor
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
25
+ Classifier: Topic :: Security
26
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
+ Classifier: Typing :: Typed
28
+ Requires-Python: >=3.9
29
+ Provides-Extra: all
30
+ Requires-Dist: numpy>=1.20; extra == 'all'
31
+ Requires-Dist: torch>=2.0; extra == 'all'
32
+ Provides-Extra: numpy
33
+ Requires-Dist: numpy>=1.20; extra == 'numpy'
34
+ Provides-Extra: test
35
+ Requires-Dist: hypothesis>=6.0; extra == 'test'
36
+ Requires-Dist: pytest>=7.0; extra == 'test'
37
+ Provides-Extra: torch
38
+ Requires-Dist: numpy>=1.20; extra == 'torch'
39
+ Requires-Dist: torch>=2.0; extra == 'torch'
40
+ Description-Content-Type: text/markdown
41
+
42
+ # MSCS — Safe Serialization for Python
43
+
44
+ **v2.3.0** | [Changelog](CHANGELOG.md) | [PyPI](https://pypi.org/project/mscs/)
45
+
46
+ > **Status: Beta** — API is stable but the format may evolve. Not yet battle-tested in large-scale production.
47
+
48
+ A secure, fast, binary serialization library. Drop-in replacement for `pickle` that **does not execute arbitrary code** during deserialization of unregistered classes.
49
+
50
+ Built for AI/ML workflows — native support for **NumPy arrays** and **PyTorch tensors** with zero-copy performance.
51
+
52
+ ## Why not pickle?
53
+
54
+ ```python
55
+ # pickle: arbitrary code execution on load
56
+ data = pickle.loads(untrusted_bytes) # can run os.system("rm -rf /")
57
+
58
+ # mscs: only reconstructs explicitly registered classes
59
+ data = mscs.loads(untrusted_bytes) # MSCSecurityError if class not registered
60
+ ```
61
+
62
+ ## Comparison with Alternatives
63
+
64
+ | Feature | mscs | pickle | safetensors | torch.save |
65
+ |---------|------|--------|-------------|------------|
66
+ | No arbitrary code execution | Partial* | No | Yes | No |
67
+ | HMAC authentication | Yes | No | No | No |
68
+ | Custom class support | Yes (registry) | Yes | No | Yes |
69
+ | NumPy arrays | Yes | Yes | Yes | Yes |
70
+ | PyTorch tensors | Yes | Yes | Yes | Yes |
71
+ | Circular references | Yes | Yes | No | Yes |
72
+ | Zero dependencies | Yes | Yes | Yes (Rust) | No |
73
+ | Compression built-in | Yes (zlib) | No | No | No |
74
+
75
+ \* **mscs executes `__setstate__`** on registered classes. See [Security Model](#security-model) for details.
76
+
77
+ **When to use safetensors instead:** If you only need to serialize tensors and arrays (model weights, embeddings), [safetensors](https://github.com/huggingface/safetensors) is the industry standard — it's written in Rust, truly zero-code-execution, and widely adopted. Use mscs when you need to serialize **mixed Python objects** (configs, custom classes, nested structures) alongside tensors.
78
+
79
+ ## Install
80
+
81
+ ```bash
82
+ pip install mscs # core (no dependencies)
83
+ pip install mscs[numpy] # + numpy support
84
+ pip install mscs[torch] # + numpy + PyTorch tensor support
85
+ pip install mscs[all] # everything
86
+ ```
87
+
88
+ ## Quick Start
89
+
90
+ ```python
91
+ import mscs
92
+
93
+ # Primitives, collections, nested structures — just works
94
+ data = {"model": "v5.2", "lr": 0.001, "layers": [64, 128, 256]}
95
+ encoded = mscs.dumps(data)
96
+ decoded = mscs.loads(encoded)
97
+
98
+ # NumPy arrays
99
+ import numpy as np
100
+ arr = np.random.randn(100, 100).astype(np.float32)
101
+ encoded = mscs.dumps(arr)
102
+
103
+ # PyTorch tensors — no .numpy() conversion needed
104
+ import torch
105
+ weights = torch.randn(256, 256)
106
+ encoded = mscs.dumps(weights) # safe, no pickle involved
107
+
108
+ # Full model checkpoints
109
+ checkpoint = {
110
+ "epoch": 100,
111
+ "model_state": {k: v for k, v in model.state_dict().items()},
112
+ "optimizer_lr": 0.0003,
113
+ }
114
+ mscs.dump(checkpoint, open("checkpoint.mscs", "wb"))
115
+ restored = mscs.load(open("checkpoint.mscs", "rb"))
116
+ ```
117
+
118
+ ## Custom Classes
119
+
120
+ ```python
121
+ import mscs
122
+ from dataclasses import dataclass
123
+
124
+ @mscs.register
125
+ @dataclass
126
+ class Config:
127
+ state_size: int = 256
128
+ lr: float = 0.001
129
+
130
+ config = Config(512, 0.0003)
131
+ data = mscs.dumps(config)
132
+ restored = mscs.loads(data) # Config(state_size=512, lr=0.0003)
133
+
134
+ # Unregistered classes raise MSCSecurityError in strict mode
135
+ mscs.loads(data_with_unknown_class) # MSCSecurityError
136
+
137
+ # Or get a dict fallback in non-strict mode
138
+ mscs.loads(data_with_unknown_class, strict=False) # {'__class__': '...', '__state__': {...}}
139
+ ```
140
+
141
+ ### Backward Compatibility with Renamed Classes
142
+
143
+ ```python
144
+ mscs.register_alias("my_module.OldConfig", Config)
145
+ ```
146
+
147
+ ### Register All Classes in a Module
148
+
149
+ ```python
150
+ import my_models
151
+ mscs.register_module(my_models)
152
+ ```
153
+
154
+ ## Compression & Integrity
155
+
156
+ ```python
157
+ # zlib compression
158
+ with open("data.mscs.z", "wb") as f:
159
+ mscs.dump_compressed(large_obj, f)
160
+
161
+ with open("data.mscs.z", "rb") as f:
162
+ obj = mscs.load_compressed(f)
163
+
164
+ # CRC32 integrity check (detects accidental corruption, NOT tamper-proof)
165
+ data = mscs.dumps(obj, with_crc=True)
166
+ mscs.loads(data) # verifies CRC, raises MSCDecodeError if corrupted
167
+
168
+ # HMAC-SHA256 authentication (cryptographic, tamper-proof)
169
+ key = b'your-secret-key-here'
170
+ data = mscs.dumps(obj, hmac_key=key)
171
+ mscs.loads(data, hmac_key=key) # verifies HMAC, raises MSCSecurityError if tampered
172
+ mscs.loads(data) # MSCSecurityError: no key provided for signed payload
173
+ mscs.loads(unsigned_data, hmac_key=key) # MSCSecurityError: anti-downgrade protection
174
+ ```
175
+
176
+ ## API Reference
177
+
178
+ ### Core
179
+
180
+ | Function | Description |
181
+ |----------|------------|
182
+ | `dumps(obj, *, with_crc=False, hmac_key=None) -> bytes` | Serialize to bytes |
183
+ | `loads(data, *, strict=True, hmac_key=None) -> Any` | Deserialize from bytes |
184
+ | `dump(obj, file, *, with_crc=False, hmac_key=None)` | Serialize to file (binary mode) |
185
+ | `load(file, *, strict=True, hmac_key=None) -> Any` | Deserialize from file |
186
+ | `dump_compressed(obj, file, level=6)` | Serialize with zlib compression |
187
+ | `load_compressed(file) -> Any` | Deserialize compressed data |
188
+
189
+ ### Registry
190
+
191
+ | Function | Description |
192
+ |----------|------------|
193
+ | `register(cls) -> cls` | Register class as safe (also works as decorator) |
194
+ | `register_alias(old_path, cls)` | Map old class path to new class |
195
+ | `register_module(module) -> list` | Register all classes in a module |
196
+
197
+ ### Utilities
198
+
199
+ | Function | Description |
200
+ |----------|------------|
201
+ | `inspect(data) -> dict` | Get metadata without deserializing |
202
+ | `benchmark(obj, rounds=100) -> dict` | Measure encode/decode performance |
203
+ | `copy(obj) -> obj` | Deep copy via serialization round-trip |
204
+
205
+ ## Supported Types
206
+
207
+ | Type | Notes |
208
+ |------|-------|
209
+ | `None`, `bool`, `int`, `float`, `complex` | Ints up to 8192 bytes (~19,700 digits) |
210
+ | `str`, `bytes`, `bytearray` | UTF-8, ref-tracked |
211
+ | `list`, `tuple`, `dict`, `set`, `frozenset` | Circular refs supported |
212
+ | `datetime`, `date`, `time`, `timedelta` | ISO 8601 |
213
+ | `Decimal`, `UUID`, `Path` | Lossless |
214
+ | `Enum` | Must be registered |
215
+ | `numpy.ndarray` | dtype whitelist enforced |
216
+ | `torch.Tensor` | Auto CPU transfer, preserves requires_grad |
217
+ | `dataclass`, `__slots__`, `__dict__` objects | Must be registered |
218
+
219
+ ## Performance
220
+
221
+ Benchmarked on a single machine (results may vary by hardware and payload):
222
+
223
+ **Payload: `state_dict` with 4 tensors (~57K parameters, dominated by contiguous float32 buffers):**
224
+
225
+ | Method | Roundtrip | Size |
226
+ |--------|-----------|------|
227
+ | **mscs** | **~0.1 ms** | **~65 KB** |
228
+ | pickle | ~0.6 ms | ~68 KB |
229
+ | torch.save | ~0.4 ms | ~67 KB |
230
+
231
+ mscs is fast for tensor-heavy payloads because it writes raw buffers with minimal framing overhead. **For small, nested Python structures (dicts, strings, configs), the speedup is smaller.** Always benchmark with your actual data.
232
+
233
+ Run `python tests/benchmark.py` to reproduce on your machine.
234
+
235
+ ## Security Model
236
+
237
+ mscs provides a **defense-in-depth** approach, but it is **not a sandbox**. Understand the boundaries:
238
+
239
+ ### What mscs prevents
240
+
241
+ 1. **No dynamic imports**: Class names in the binary stream are only used as registry lookup keys — never passed to `importlib`
242
+ 2. **Explicit registry**: Custom classes must be registered before deserialization; unregistered classes raise `MSCSecurityError`
243
+ 3. **NumPy dtype whitelist**: Blocks `object`, `void`, and structured dtypes that could execute code
244
+ 4. **Configurable limits**: `MAX_DEPTH=256`, `MAX_SIZE=512MB`, `MAX_COLLECTION=10M`, `MAX_INT_BYTES=8192`
245
+ 5. **Anti zip-bomb**: `load_compressed` validates both compressed and decompressed sizes with bounded reads
246
+ 6. **Path null byte rejection**: Paths containing null bytes are rejected
247
+ 7. **CRC32 corruption detection**: Optional checksum to detect accidental data corruption (not cryptographic — an attacker can forge CRC32)
248
+ 8. **HMAC-SHA256 authentication**: Optional cryptographic signature to detect intentional tampering. Anti-downgrade protection prevents stripping the HMAC flag.
249
+ 9. **Trailing bytes rejection**: Payloads with unexpected bytes after the serialized object are rejected
250
+ 10. **Integer size limit**: Ints larger than `MAX_INT_BYTES` (8192 bytes, ~19,700 digits) are rejected to prevent CPU exhaustion attacks
251
+
252
+ ### What mscs does NOT prevent
253
+
254
+ 1. **`__setstate__` execution**: If you register a class that implements `__setstate__`, that method **will execute** during deserialization. Only register classes you trust.
255
+ 2. **Path traversal**: Deserialized `Path` objects may contain `../` sequences. The consumer must validate paths before using them for file I/O.
256
+ 3. **Malicious registered classes**: The security boundary is the registry. If you register a class with dangerous behavior in `__init__`, `__setstate__`, or property setters, mscs cannot protect you.
257
+
258
+ **Rule of thumb**: mscs is safe for deserializing untrusted *data* as long as your registry only contains trusted *classes*.
259
+
260
+ ## Binary Format
261
+
262
+ ```
263
+ ┌──────────┬─────────┬───────┬──────────┬────────────────┬──────────────┬──────────────┐
264
+ │ Magic(4) │ Ver.(1) │ Fl(1) │ Tag(1) │ Payload(var) │ CRC32(4)? │ HMAC(32)? │
265
+ │ "MSCS" │ 0x02 │ bits │ type tag │ type-dependent │ if flag 0x01 │ if flag 0x02 │
266
+ └──────────┴─────────┴───────┴──────────┴────────────────┴──────────────┴──────────────┘
267
+ ```
268
+
269
+ **Header** (6 bytes fixed):
270
+ - Bytes 0-3: Magic `MSCS` (0x4D534353)
271
+ - Byte 4: Format version (currently `0x02`)
272
+ - Byte 5: Flags (bit 0 = CRC32 appended, bit 1 = HMAC-SHA256 appended)
273
+
274
+ CRC32 and HMAC are mutually exclusive (HMAC is strictly superior).
275
+
276
+ **Payload**: Recursive type-length-value encoding. Each value starts with a 1-byte type tag:
277
+
278
+ | Tag | Type | Payload format |
279
+ |-----|------|----------------|
280
+ | 0x00 | None | (empty) |
281
+ | 0x01 | bool | 1 byte (0x00/0x01) |
282
+ | 0x02 | int | `<H>` byte count + signed little-endian bytes |
283
+ | 0x03 | float | `<d>` IEEE 754 double |
284
+ | 0x04 | str | `<I>` byte count + UTF-8 |
285
+ | 0x05 | bytes | `<I>` byte count + raw |
286
+ | 0x06 | list | `<I>` item count + items |
287
+ | 0x07 | tuple | `<I>` item count + items |
288
+ | 0x08 | dict | `<I>` pair count + key/value pairs |
289
+ | 0x09 | set | `<I>` item count + items (sorted) |
290
+ | 0x0A | ndarray | str(meta) + `<I>` data size + raw buffer |
291
+ | 0x0B | object | str(class_path) + encoded(state) |
292
+ | 0x0C | complex | `<dd>` real, imag |
293
+ | 0x0D | frozenset | `<I>` item count + items (sorted) |
294
+ | 0x0E | datetime | `<H>` str len + ISO 8601 string |
295
+ | 0x0F | date | `<HBB>` year, month, day |
296
+ | 0x10 | time | `<H>` str len + ISO 8601 string |
297
+ | 0x11 | timedelta (legacy) | `<iiI>` days, seconds, microseconds |
298
+ | 0x12 | Decimal | `<H>` str len + decimal string |
299
+ | 0x13 | Enum | str(class_path) + encoded(value) |
300
+ | 0x14 | bytearray | `<I>` byte count + raw |
301
+ | 0x15 | ref | `<I>` reference ID |
302
+ | 0x16 | UUID | 16 bytes raw |
303
+ | 0x17 | Path | `<I>` str len + UTF-8 path string |
304
+ | 0x18 | Tensor | str(meta) + `<I>` data size + raw buffer |
305
+ | 0x19 | timedelta2 | `<iiI>` days, seconds, microseconds |
306
+
307
+ **ndarray meta**: `"{dtype}|{shape}"` where shape is `"dim0xdim1x..."` (e.g., `"float32|100x100"`).
308
+
309
+ **Tensor meta**: `"{dtype}|{shape}|{requires_grad}"` (e.g., `"float32|256x256|0"`).
310
+
311
+ **Reference tracking**: Mutable containers (list, dict, set, etc.), strings, bytes, and arrays are assigned incrementing IDs. Tag 0x15 refers back to a previously seen object by ID, enabling circular reference support.
312
+
313
+ ## License
314
+
315
+ MIT
mscs-2.3.0/README.md ADDED
@@ -0,0 +1,274 @@
1
+ # MSCS — Safe Serialization for Python
2
+
3
+ **v2.3.0** | [Changelog](CHANGELOG.md) | [PyPI](https://pypi.org/project/mscs/)
4
+
5
+ > **Status: Beta** — API is stable but the format may evolve. Not yet battle-tested in large-scale production.
6
+
7
+ A secure, fast, binary serialization library. Drop-in replacement for `pickle` that **does not execute arbitrary code** during deserialization of unregistered classes.
8
+
9
+ Built for AI/ML workflows — native support for **NumPy arrays** and **PyTorch tensors** with zero-copy performance.
10
+
11
+ ## Why not pickle?
12
+
13
+ ```python
14
+ # pickle: arbitrary code execution on load
15
+ data = pickle.loads(untrusted_bytes) # can run os.system("rm -rf /")
16
+
17
+ # mscs: only reconstructs explicitly registered classes
18
+ data = mscs.loads(untrusted_bytes) # MSCSecurityError if class not registered
19
+ ```
20
+
21
+ ## Comparison with Alternatives
22
+
23
+ | Feature | mscs | pickle | safetensors | torch.save |
24
+ |---------|------|--------|-------------|------------|
25
+ | No arbitrary code execution | Partial* | No | Yes | No |
26
+ | HMAC authentication | Yes | No | No | No |
27
+ | Custom class support | Yes (registry) | Yes | No | Yes |
28
+ | NumPy arrays | Yes | Yes | Yes | Yes |
29
+ | PyTorch tensors | Yes | Yes | Yes | Yes |
30
+ | Circular references | Yes | Yes | No | Yes |
31
+ | Zero dependencies | Yes | Yes | Yes (Rust) | No |
32
+ | Compression built-in | Yes (zlib) | No | No | No |
33
+
34
+ \* **mscs executes `__setstate__`** on registered classes. See [Security Model](#security-model) for details.
35
+
36
+ **When to use safetensors instead:** If you only need to serialize tensors and arrays (model weights, embeddings), [safetensors](https://github.com/huggingface/safetensors) is the industry standard — it's written in Rust, truly zero-code-execution, and widely adopted. Use mscs when you need to serialize **mixed Python objects** (configs, custom classes, nested structures) alongside tensors.
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ pip install mscs # core (no dependencies)
42
+ pip install mscs[numpy] # + numpy support
43
+ pip install mscs[torch] # + numpy + PyTorch tensor support
44
+ pip install mscs[all] # everything
45
+ ```
46
+
47
+ ## Quick Start
48
+
49
+ ```python
50
+ import mscs
51
+
52
+ # Primitives, collections, nested structures — just works
53
+ data = {"model": "v5.2", "lr": 0.001, "layers": [64, 128, 256]}
54
+ encoded = mscs.dumps(data)
55
+ decoded = mscs.loads(encoded)
56
+
57
+ # NumPy arrays
58
+ import numpy as np
59
+ arr = np.random.randn(100, 100).astype(np.float32)
60
+ encoded = mscs.dumps(arr)
61
+
62
+ # PyTorch tensors — no .numpy() conversion needed
63
+ import torch
64
+ weights = torch.randn(256, 256)
65
+ encoded = mscs.dumps(weights) # safe, no pickle involved
66
+
67
+ # Full model checkpoints
68
+ checkpoint = {
69
+ "epoch": 100,
70
+ "model_state": {k: v for k, v in model.state_dict().items()},
71
+ "optimizer_lr": 0.0003,
72
+ }
73
+ mscs.dump(checkpoint, open("checkpoint.mscs", "wb"))
74
+ restored = mscs.load(open("checkpoint.mscs", "rb"))
75
+ ```
76
+
77
+ ## Custom Classes
78
+
79
+ ```python
80
+ import mscs
81
+ from dataclasses import dataclass
82
+
83
+ @mscs.register
84
+ @dataclass
85
+ class Config:
86
+ state_size: int = 256
87
+ lr: float = 0.001
88
+
89
+ config = Config(512, 0.0003)
90
+ data = mscs.dumps(config)
91
+ restored = mscs.loads(data) # Config(state_size=512, lr=0.0003)
92
+
93
+ # Unregistered classes raise MSCSecurityError in strict mode
94
+ mscs.loads(data_with_unknown_class) # MSCSecurityError
95
+
96
+ # Or get a dict fallback in non-strict mode
97
+ mscs.loads(data_with_unknown_class, strict=False) # {'__class__': '...', '__state__': {...}}
98
+ ```
99
+
100
+ ### Backward Compatibility with Renamed Classes
101
+
102
+ ```python
103
+ mscs.register_alias("my_module.OldConfig", Config)
104
+ ```
105
+
106
+ ### Register All Classes in a Module
107
+
108
+ ```python
109
+ import my_models
110
+ mscs.register_module(my_models)
111
+ ```
112
+
113
+ ## Compression & Integrity
114
+
115
+ ```python
116
+ # zlib compression
117
+ with open("data.mscs.z", "wb") as f:
118
+ mscs.dump_compressed(large_obj, f)
119
+
120
+ with open("data.mscs.z", "rb") as f:
121
+ obj = mscs.load_compressed(f)
122
+
123
+ # CRC32 integrity check (detects accidental corruption, NOT tamper-proof)
124
+ data = mscs.dumps(obj, with_crc=True)
125
+ mscs.loads(data) # verifies CRC, raises MSCDecodeError if corrupted
126
+
127
+ # HMAC-SHA256 authentication (cryptographic, tamper-proof)
128
+ key = b'your-secret-key-here'
129
+ data = mscs.dumps(obj, hmac_key=key)
130
+ mscs.loads(data, hmac_key=key) # verifies HMAC, raises MSCSecurityError if tampered
131
+ mscs.loads(data) # MSCSecurityError: no key provided for signed payload
132
+ mscs.loads(unsigned_data, hmac_key=key) # MSCSecurityError: anti-downgrade protection
133
+ ```
134
+
135
+ ## API Reference
136
+
137
+ ### Core
138
+
139
+ | Function | Description |
140
+ |----------|------------|
141
+ | `dumps(obj, *, with_crc=False, hmac_key=None) -> bytes` | Serialize to bytes |
142
+ | `loads(data, *, strict=True, hmac_key=None) -> Any` | Deserialize from bytes |
143
+ | `dump(obj, file, *, with_crc=False, hmac_key=None)` | Serialize to file (binary mode) |
144
+ | `load(file, *, strict=True, hmac_key=None) -> Any` | Deserialize from file |
145
+ | `dump_compressed(obj, file, level=6)` | Serialize with zlib compression |
146
+ | `load_compressed(file) -> Any` | Deserialize compressed data |
147
+
148
+ ### Registry
149
+
150
+ | Function | Description |
151
+ |----------|------------|
152
+ | `register(cls) -> cls` | Register class as safe (also works as decorator) |
153
+ | `register_alias(old_path, cls)` | Map old class path to new class |
154
+ | `register_module(module) -> list` | Register all classes in a module |
155
+
156
+ ### Utilities
157
+
158
+ | Function | Description |
159
+ |----------|------------|
160
+ | `inspect(data) -> dict` | Get metadata without deserializing |
161
+ | `benchmark(obj, rounds=100) -> dict` | Measure encode/decode performance |
162
+ | `copy(obj) -> obj` | Deep copy via serialization round-trip |
163
+
164
+ ## Supported Types
165
+
166
+ | Type | Notes |
167
+ |------|-------|
168
+ | `None`, `bool`, `int`, `float`, `complex` | Ints up to 8192 bytes (~19,700 digits) |
169
+ | `str`, `bytes`, `bytearray` | UTF-8, ref-tracked |
170
+ | `list`, `tuple`, `dict`, `set`, `frozenset` | Circular refs supported |
171
+ | `datetime`, `date`, `time`, `timedelta` | ISO 8601 |
172
+ | `Decimal`, `UUID`, `Path` | Lossless |
173
+ | `Enum` | Must be registered |
174
+ | `numpy.ndarray` | dtype whitelist enforced |
175
+ | `torch.Tensor` | Auto CPU transfer, preserves requires_grad |
176
+ | `dataclass`, `__slots__`, `__dict__` objects | Must be registered |
177
+
178
+ ## Performance
179
+
180
+ Benchmarked on a single machine (results may vary by hardware and payload):
181
+
182
+ **Payload: `state_dict` with 4 tensors (~57K parameters, dominated by contiguous float32 buffers):**
183
+
184
+ | Method | Roundtrip | Size |
185
+ |--------|-----------|------|
186
+ | **mscs** | **~0.1 ms** | **~65 KB** |
187
+ | pickle | ~0.6 ms | ~68 KB |
188
+ | torch.save | ~0.4 ms | ~67 KB |
189
+
190
+ mscs is fast for tensor-heavy payloads because it writes raw buffers with minimal framing overhead. **For small, nested Python structures (dicts, strings, configs), the speedup is smaller.** Always benchmark with your actual data.
191
+
192
+ Run `python tests/benchmark.py` to reproduce on your machine.
193
+
194
+ ## Security Model
195
+
196
+ mscs provides a **defense-in-depth** approach, but it is **not a sandbox**. Understand the boundaries:
197
+
198
+ ### What mscs prevents
199
+
200
+ 1. **No dynamic imports**: Class names in the binary stream are only used as registry lookup keys — never passed to `importlib`
201
+ 2. **Explicit registry**: Custom classes must be registered before deserialization; unregistered classes raise `MSCSecurityError`
202
+ 3. **NumPy dtype whitelist**: Blocks `object`, `void`, and structured dtypes that could execute code
203
+ 4. **Configurable limits**: `MAX_DEPTH=256`, `MAX_SIZE=512MB`, `MAX_COLLECTION=10M`, `MAX_INT_BYTES=8192`
204
+ 5. **Anti zip-bomb**: `load_compressed` validates both compressed and decompressed sizes with bounded reads
205
+ 6. **Path null byte rejection**: Paths containing null bytes are rejected
206
+ 7. **CRC32 corruption detection**: Optional checksum to detect accidental data corruption (not cryptographic — an attacker can forge CRC32)
207
+ 8. **HMAC-SHA256 authentication**: Optional cryptographic signature to detect intentional tampering. Anti-downgrade protection prevents stripping the HMAC flag.
208
+ 9. **Trailing bytes rejection**: Payloads with unexpected bytes after the serialized object are rejected
209
+ 10. **Integer size limit**: Ints larger than `MAX_INT_BYTES` (8192 bytes, ~19,700 digits) are rejected to prevent CPU exhaustion attacks
210
+
211
+ ### What mscs does NOT prevent
212
+
213
+ 1. **`__setstate__` execution**: If you register a class that implements `__setstate__`, that method **will execute** during deserialization. Only register classes you trust.
214
+ 2. **Path traversal**: Deserialized `Path` objects may contain `../` sequences. The consumer must validate paths before using them for file I/O.
215
+ 3. **Malicious registered classes**: The security boundary is the registry. If you register a class with dangerous behavior in `__init__`, `__setstate__`, or property setters, mscs cannot protect you.
216
+
217
+ **Rule of thumb**: mscs is safe for deserializing untrusted *data* as long as your registry only contains trusted *classes*.
218
+
219
+ ## Binary Format
220
+
221
+ ```
222
+ ┌──────────┬─────────┬───────┬──────────┬────────────────┬──────────────┬──────────────┐
223
+ │ Magic(4) │ Ver.(1) │ Fl(1) │ Tag(1) │ Payload(var) │ CRC32(4)? │ HMAC(32)? │
224
+ │ "MSCS" │ 0x02 │ bits │ type tag │ type-dependent │ if flag 0x01 │ if flag 0x02 │
225
+ └──────────┴─────────┴───────┴──────────┴────────────────┴──────────────┴──────────────┘
226
+ ```
227
+
228
+ **Header** (6 bytes fixed):
229
+ - Bytes 0-3: Magic `MSCS` (0x4D534353)
230
+ - Byte 4: Format version (currently `0x02`)
231
+ - Byte 5: Flags (bit 0 = CRC32 appended, bit 1 = HMAC-SHA256 appended)
232
+
233
+ CRC32 and HMAC are mutually exclusive (HMAC is strictly superior).
234
+
235
+ **Payload**: Recursive type-length-value encoding. Each value starts with a 1-byte type tag:
236
+
237
+ | Tag | Type | Payload format |
238
+ |-----|------|----------------|
239
+ | 0x00 | None | (empty) |
240
+ | 0x01 | bool | 1 byte (0x00/0x01) |
241
+ | 0x02 | int | `<H>` byte count + signed little-endian bytes |
242
+ | 0x03 | float | `<d>` IEEE 754 double |
243
+ | 0x04 | str | `<I>` byte count + UTF-8 |
244
+ | 0x05 | bytes | `<I>` byte count + raw |
245
+ | 0x06 | list | `<I>` item count + items |
246
+ | 0x07 | tuple | `<I>` item count + items |
247
+ | 0x08 | dict | `<I>` pair count + key/value pairs |
248
+ | 0x09 | set | `<I>` item count + items (sorted) |
249
+ | 0x0A | ndarray | str(meta) + `<I>` data size + raw buffer |
250
+ | 0x0B | object | str(class_path) + encoded(state) |
251
+ | 0x0C | complex | `<dd>` real, imag |
252
+ | 0x0D | frozenset | `<I>` item count + items (sorted) |
253
+ | 0x0E | datetime | `<H>` str len + ISO 8601 string |
254
+ | 0x0F | date | `<HBB>` year, month, day |
255
+ | 0x10 | time | `<H>` str len + ISO 8601 string |
256
+ | 0x11 | timedelta (legacy) | `<iiI>` days, seconds, microseconds |
257
+ | 0x12 | Decimal | `<H>` str len + decimal string |
258
+ | 0x13 | Enum | str(class_path) + encoded(value) |
259
+ | 0x14 | bytearray | `<I>` byte count + raw |
260
+ | 0x15 | ref | `<I>` reference ID |
261
+ | 0x16 | UUID | 16 bytes raw |
262
+ | 0x17 | Path | `<I>` str len + UTF-8 path string |
263
+ | 0x18 | Tensor | str(meta) + `<I>` data size + raw buffer |
264
+ | 0x19 | timedelta2 | `<iiI>` days, seconds, microseconds |
265
+
266
+ **ndarray meta**: `"{dtype}|{shape}"` where shape is `"dim0xdim1x..."` (e.g., `"float32|100x100"`).
267
+
268
+ **Tensor meta**: `"{dtype}|{shape}|{requires_grad}"` (e.g., `"float32|256x256|0"`).
269
+
270
+ **Reference tracking**: Mutable containers (list, dict, set, etc.), strings, bytes, and arrays are assigned incrementing IDs. Tag 0x15 refers back to a previously seen object by ID, enabling circular reference support.
271
+
272
+ ## License
273
+
274
+ MIT
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "mscs"
7
- version = "2.2.1"
8
- description = "Safe, fast serialization for Python — a secure replacement for pickle with native support for numpy arrays and PyTorch tensors."
7
+ version = "2.3.0"
8
+ description = "Safe, fast serialization for Python — a secure replacement for pickle with HMAC authentication, native numpy/PyTorch support."
9
9
  readme = "README.md"
10
10
  license = "MIT"
11
11
  requires-python = ">=3.9"
@@ -13,9 +13,9 @@ authors = [
13
13
  { name = "Esraderey" },
14
14
  ]
15
15
  keywords = [
16
- "serialization", "pickle", "safe", "secure",
16
+ "serialization", "pickle", "safe", "secure", "hmac",
17
17
  "numpy", "pytorch", "tensor", "checkpoint",
18
- "binary", "fast",
18
+ "binary", "fast", "integrity",
19
19
  ]
20
20
  classifiers = [
21
21
  "Development Status :: 4 - Beta",
@@ -39,11 +39,13 @@ classifiers = [
39
39
  numpy = ["numpy>=1.20"]
40
40
  torch = ["numpy>=1.20", "torch>=2.0"]
41
41
  all = ["numpy>=1.20", "torch>=2.0"]
42
+ test = ["pytest>=7.0", "hypothesis>=6.0"]
42
43
 
43
44
  [project.urls]
44
45
  Homepage = "https://github.com/ElEscribanoSilente/MSC-Serial"
45
46
  Repository = "https://github.com/ElEscribanoSilente/MSC-Serial"
46
47
  Issues = "https://github.com/ElEscribanoSilente/MSC-Serial/issues"
48
+ Changelog = "https://github.com/ElEscribanoSilente/MSC-Serial/blob/main/CHANGELOG.md"
47
49
 
48
50
  [tool.hatch.build.targets.sdist]
49
51
  include = ["src/mscs/"]
@@ -38,6 +38,7 @@ from mscs._core import (
38
38
  MAX_COMPRESSED,
39
39
  MAX_COLLECTION,
40
40
  MAX_STRING,
41
+ MAX_INT_BYTES,
41
42
  )
42
43
 
43
44
  __all__ = [
@@ -47,4 +48,5 @@ __all__ = [
47
48
  "register", "register_alias", "register_module",
48
49
  "inspect", "benchmark", "copy",
49
50
  "MSCError", "MSCEncodeError", "MSCDecodeError", "MSCSecurityError",
51
+ "MAX_INT_BYTES",
50
52
  ]