ntcompress 0.3.0__py3-none-any.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.
- ntcompress/__init__.py +20 -0
- ntcompress/_types.py +8 -0
- ntcompress/ese/__init__.py +216 -0
- ntcompress/ese/_registry.py +32 -0
- ntcompress/ese/_sevenbit.py +167 -0
- ntcompress/ese/checksums.py +97 -0
- ntcompress/ese/lz4.py +364 -0
- ntcompress/ese/scrub.py +182 -0
- ntcompress/ese/sevenbit_ascii.py +55 -0
- ntcompress/ese/sevenbit_unicode.py +82 -0
- ntcompress/ese/xpress.py +125 -0
- ntcompress/ese/xpress10.py +155 -0
- ntcompress/ese/xpress9.py +1866 -0
- ntcompress/exceptions.py +55 -0
- ntcompress/ntdll/__init__.py +124 -0
- ntcompress/ntdll/_registry.py +32 -0
- ntcompress/ntdll/deflate.py +55 -0
- ntcompress/ntdll/lznt1.py +228 -0
- ntcompress/ntdll/xp10.py +49 -0
- ntcompress/ntdll/xpress.py +317 -0
- ntcompress/ntdll/xpress9.py +357 -0
- ntcompress/ntdll/xpress_huff.py +489 -0
- ntcompress/ntdll/zlib.py +45 -0
- ntcompress/py.typed +0 -0
- ntcompress-0.3.0.dist-info/METADATA +150 -0
- ntcompress-0.3.0.dist-info/RECORD +29 -0
- ntcompress-0.3.0.dist-info/WHEEL +4 -0
- ntcompress-0.3.0.dist-info/licenses/LICENSE +202 -0
- ntcompress-0.3.0.dist-info/licenses/NOTICE +14 -0
ntcompress/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Pure-Python codecs for Microsoft ESE record-compression and Windows ntdll compression formats.
|
|
2
|
+
|
|
3
|
+
``ntcompress`` compresses and decompresses all ESE record schemes (7-bit ASCII/Unicode,
|
|
4
|
+
XPRESS, SCRUB, XPRESS9, XPRESS10, LZ4) and all ntdll ``RtlCompressBuffer`` formats
|
|
5
|
+
(LZNT1, XPRESS, XPRESS_HUFF, Compact XPRESS9, XP10, DEFLATE, ZLIB). Compress output is
|
|
6
|
+
verified byte-identical to ``esent.dll`` and ``ntdll.dll`` across 16 Windows builds. No
|
|
7
|
+
runtime dependencies beyond the standard library.
|
|
8
|
+
|
|
9
|
+
ESE codecs live under :mod:`ntcompress.ese`; ntdll standalone codecs under
|
|
10
|
+
:mod:`ntcompress.ntdll`.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
__version__ = version("ntcompress")
|
|
19
|
+
except PackageNotFoundError: # pragma: no cover - source checkout without install metadata
|
|
20
|
+
__version__ = "0.0.0"
|
ntcompress/_types.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""ESE (Extensible Storage Engine) record-compression formats.
|
|
2
|
+
|
|
3
|
+
Provides both Shape A (enum dispatch) and Shape B (direct module import) APIs for
|
|
4
|
+
every ESE record-compression scheme. The ``Format`` enum values are the 5-bit scheme
|
|
5
|
+
IDs extracted from the first byte of a compressed ESE cell, matching the
|
|
6
|
+
``CDataCompressor::COMPRESSION_SCHEME`` constants in the MIT ESE source.
|
|
7
|
+
|
|
8
|
+
Shape A (enum dispatch)::
|
|
9
|
+
|
|
10
|
+
import ntcompress.ese
|
|
11
|
+
compressed = ntcompress.ese.compress(data, ntcompress.ese.Format.XPRESS)
|
|
12
|
+
plain = ntcompress.ese.decompress(compressed) # auto-detects format
|
|
13
|
+
|
|
14
|
+
Shape B (direct module)::
|
|
15
|
+
|
|
16
|
+
from ntcompress.ese import xpress
|
|
17
|
+
compressed = xpress.compress(data)
|
|
18
|
+
plain = xpress.decompress(compressed)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from enum import IntEnum
|
|
24
|
+
from typing import TYPE_CHECKING, Final
|
|
25
|
+
|
|
26
|
+
from ntcompress.ese._registry import _get, _register
|
|
27
|
+
from ntcompress.exceptions import (
|
|
28
|
+
CompressionError,
|
|
29
|
+
DecompressionError,
|
|
30
|
+
FormatUnavailableError,
|
|
31
|
+
ScrubDetectedError,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from ntcompress._types import Buffer
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# --- Header bit layout ---
|
|
39
|
+
|
|
40
|
+
_SCHEME_SHIFT: Final = 3
|
|
41
|
+
"""Bit offset of the 5-bit format id within the header byte (compression.cxx:2003)."""
|
|
42
|
+
|
|
43
|
+
_FLAG_MASK: Final = 0b0000_0111
|
|
44
|
+
"""Mask selecting the low 3 format-specific flag bits of the header byte."""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Format(IntEnum):
|
|
48
|
+
"""ESE record-compression format identifiers.
|
|
49
|
+
|
|
50
|
+
Values are the 5-bit scheme IDs extracted from the first byte of a compressed
|
|
51
|
+
ESE cell. See ``CDataCompressor::COMPRESSION_SCHEME`` in the MIT-licensed ESE
|
|
52
|
+
source (``compression.cxx:504-512``).
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
NONE = 0x00
|
|
56
|
+
"""Sentinel -- uncompressed cell, no compression header."""
|
|
57
|
+
|
|
58
|
+
SEVEN_BIT_ASCII = 0x01
|
|
59
|
+
"""7-bit ASCII packing (compression.cxx:1168-1387)."""
|
|
60
|
+
|
|
61
|
+
SEVEN_BIT_UNICODE = 0x02
|
|
62
|
+
"""7-bit Unicode packing over UTF-16LE code units (compression.cxx:1390-1504)."""
|
|
63
|
+
|
|
64
|
+
XPRESS = 0x03
|
|
65
|
+
"""Plain LZ77 ([MS-XCA] §2.1) with 3-byte ESE frame (compression.cxx:1507-1568)."""
|
|
66
|
+
|
|
67
|
+
SCRUB = 0x04
|
|
68
|
+
"""Erase marker -- not a compression format. Use :mod:`ntcompress.ese.scrub`."""
|
|
69
|
+
|
|
70
|
+
XPRESS9 = 0x05
|
|
71
|
+
"""LZ77+Huffman9 with ESE frame (compression.cxx:1686-1759)."""
|
|
72
|
+
|
|
73
|
+
XPRESS10 = 0x06
|
|
74
|
+
"""LZ4 block + CRC-32C/CRC-64 integrity (compression.cxx:1935-2064)."""
|
|
75
|
+
|
|
76
|
+
LZ4 = 0x07
|
|
77
|
+
"""Raw LZ4 block with 3-byte ESE frame (compression.cxx:2070-2109)."""
|
|
78
|
+
|
|
79
|
+
MAXIMUM = 0x1F
|
|
80
|
+
"""Sentinel -- upper bound of the 5-bit scheme space, not a real format."""
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# --- First-byte helpers ---
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def format_id(first_byte: int) -> int:
|
|
87
|
+
"""Extract the 5-bit format ID from an ESE cell's header byte.
|
|
88
|
+
|
|
89
|
+
Returns ``first_byte >> 3`` (0-31). Callers map this to a :class:`Format` member.
|
|
90
|
+
"""
|
|
91
|
+
return first_byte >> _SCHEME_SHIFT
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def format_flags(first_byte: int) -> int:
|
|
95
|
+
"""Extract the 3 format-specific flag bits from an ESE cell's header byte.
|
|
96
|
+
|
|
97
|
+
Returns ``first_byte & 0x7``. Meaning is format-specific.
|
|
98
|
+
"""
|
|
99
|
+
return first_byte & _FLAG_MASK
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def header_byte(fmt: Format, flags: int = 0) -> int:
|
|
103
|
+
"""Build a record header byte from a format id and optional flag bits.
|
|
104
|
+
|
|
105
|
+
Returns ``(fmt << 3) | (flags & 0x7)``.
|
|
106
|
+
"""
|
|
107
|
+
return (fmt << _SCHEME_SHIFT) | (flags & _FLAG_MASK)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# --- Shape A dispatch ---
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _format_of(blob: Buffer) -> Format:
|
|
114
|
+
"""Resolve a buffer's leading byte to a :class:`Format`, or raise."""
|
|
115
|
+
if len(blob) == 0:
|
|
116
|
+
msg = "cannot read a compression format from an empty buffer"
|
|
117
|
+
raise DecompressionError(msg)
|
|
118
|
+
raw = format_id(blob[0])
|
|
119
|
+
try:
|
|
120
|
+
fmt = Format(raw)
|
|
121
|
+
except ValueError:
|
|
122
|
+
msg = f"unknown ESE compression format id 0x{raw:x}"
|
|
123
|
+
raise DecompressionError(msg) from None
|
|
124
|
+
if fmt is Format.MAXIMUM:
|
|
125
|
+
msg = f"Format.MAXIMUM (0x{raw:x}) is a sentinel value, not a compression format"
|
|
126
|
+
raise DecompressionError(msg)
|
|
127
|
+
return fmt
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def compress(data: Buffer, fmt: Format) -> bytes:
|
|
131
|
+
"""Encode plaintext into a framed ESE cell using the specified format.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
data: The plaintext to compress.
|
|
135
|
+
fmt: The ESE compression format to use.
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
CompressionError: The format is a sentinel (NONE, SCRUB, MAXIMUM), or the
|
|
139
|
+
input cannot be encoded by the requested format.
|
|
140
|
+
FormatUnavailableError: No codec is registered, or the codec does not
|
|
141
|
+
support compression (decode-only).
|
|
142
|
+
"""
|
|
143
|
+
if fmt is Format.NONE:
|
|
144
|
+
msg = "Format.NONE is not a compression format"
|
|
145
|
+
raise CompressionError(msg)
|
|
146
|
+
if fmt is Format.SCRUB:
|
|
147
|
+
msg = "Format.SCRUB is not a compression format — use ntcompress.ese.scrub"
|
|
148
|
+
raise CompressionError(msg)
|
|
149
|
+
if fmt is Format.MAXIMUM:
|
|
150
|
+
msg = "Format.MAXIMUM is a sentinel value, not a compression format"
|
|
151
|
+
raise CompressionError(msg)
|
|
152
|
+
module = _get(fmt)
|
|
153
|
+
if not hasattr(module, "compress"):
|
|
154
|
+
msg = f"format {fmt.name} (0x{fmt.value:x}) is decode-only; no encoder is available"
|
|
155
|
+
raise FormatUnavailableError(msg)
|
|
156
|
+
return module.compress(data)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def decompress(blob: Buffer, fmt: Format | None = None) -> bytes:
|
|
160
|
+
"""Decode a framed ESE cell.
|
|
161
|
+
|
|
162
|
+
When ``fmt`` is None, the format is auto-detected from the header byte.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
blob: The framed cell, including the header byte.
|
|
166
|
+
fmt: The format to use, or None for auto-detection.
|
|
167
|
+
|
|
168
|
+
Raises:
|
|
169
|
+
DecompressionError: Empty buffer, unknown format id, or decode failure.
|
|
170
|
+
ScrubDetectedError: The record is a SCRUB (0x4) erase marker.
|
|
171
|
+
FormatUnavailableError: The format is known but no codec is registered.
|
|
172
|
+
"""
|
|
173
|
+
if fmt is None:
|
|
174
|
+
fmt = _format_of(blob)
|
|
175
|
+
if fmt is Format.NONE:
|
|
176
|
+
msg = "Format.NONE has no compression header; pass the raw cell body instead"
|
|
177
|
+
raise DecompressionError(msg)
|
|
178
|
+
if fmt is Format.SCRUB:
|
|
179
|
+
msg = "record is a SCRUB erase marker; no plaintext is recoverable — use ntcompress.ese.scrub"
|
|
180
|
+
raise ScrubDetectedError(msg)
|
|
181
|
+
if fmt is Format.MAXIMUM:
|
|
182
|
+
msg = "Format.MAXIMUM is a sentinel value, not a compression format"
|
|
183
|
+
raise DecompressionError(msg)
|
|
184
|
+
return _get(fmt).decompress(blob)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def decompressed_size(blob: Buffer) -> int:
|
|
188
|
+
"""Return a framed cell's recorded plaintext length without decoding.
|
|
189
|
+
|
|
190
|
+
Auto-detects the format from the header byte.
|
|
191
|
+
|
|
192
|
+
Raises:
|
|
193
|
+
DecompressionError: Empty buffer or unknown format id.
|
|
194
|
+
ScrubDetectedError: The record is a SCRUB erase marker.
|
|
195
|
+
FormatUnavailableError: The format is known but no codec is registered.
|
|
196
|
+
"""
|
|
197
|
+
fmt = _format_of(blob)
|
|
198
|
+
if fmt is Format.NONE:
|
|
199
|
+
msg = "Format.NONE has no compression header; the size is the raw body length"
|
|
200
|
+
raise DecompressionError(msg)
|
|
201
|
+
if fmt is Format.SCRUB:
|
|
202
|
+
msg = "record is a SCRUB erase marker; it has no decompressed size — use ntcompress.ese.scrub"
|
|
203
|
+
raise ScrubDetectedError(msg)
|
|
204
|
+
return _get(fmt).decompressed_size(blob)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# --- Auto-registration of codec modules ---
|
|
208
|
+
|
|
209
|
+
from ntcompress.ese import lz4, sevenbit_ascii, sevenbit_unicode, xpress, xpress9, xpress10 # noqa: E402
|
|
210
|
+
|
|
211
|
+
_register(Format.SEVEN_BIT_ASCII, sevenbit_ascii)
|
|
212
|
+
_register(Format.SEVEN_BIT_UNICODE, sevenbit_unicode)
|
|
213
|
+
_register(Format.XPRESS, xpress)
|
|
214
|
+
_register(Format.XPRESS9, xpress9)
|
|
215
|
+
_register(Format.XPRESS10, xpress10)
|
|
216
|
+
_register(Format.LZ4, lz4)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Internal dispatch table for ESE compression formats.
|
|
2
|
+
|
|
3
|
+
Maps :class:`Format` enum members to the modules that implement them. Each codec
|
|
4
|
+
module is imported and registered when :mod:`ntcompress.ese` is first loaded.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from ntcompress.exceptions import FormatUnavailableError
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from types import ModuleType
|
|
15
|
+
|
|
16
|
+
from ntcompress.ese import Format
|
|
17
|
+
|
|
18
|
+
_CODECS: dict[Format, ModuleType] = {}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _register(fmt: Format, module: ModuleType) -> None:
|
|
22
|
+
"""Register a codec module for a given format."""
|
|
23
|
+
_CODECS[fmt] = module
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _get(fmt: Format) -> ModuleType:
|
|
27
|
+
"""Look up the codec module for a format, or raise."""
|
|
28
|
+
module = _CODECS.get(fmt)
|
|
29
|
+
if module is None:
|
|
30
|
+
msg = f"no codec registered for ESE format {fmt.name} (0x{fmt.value:x})"
|
|
31
|
+
raise FormatUnavailableError(msg)
|
|
32
|
+
return module
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Internal shared helpers for 7-bit ASCII and Unicode codecs.
|
|
2
|
+
|
|
3
|
+
These two schemes share their framing and packer, so the common infrastructure lives
|
|
4
|
+
here. Each packs 7 significant bits per source unit LSB-first into a continuous
|
|
5
|
+
bitstream: 7BITASCII over bytes, 7BITUNICODE over UTF-16LE code units whose high byte
|
|
6
|
+
is zero (so the wide scheme differs only in its 2-byte source stride and in re-emitting
|
|
7
|
+
the ``0x00`` high byte on decode). The low 3 bits of the header byte hold the number of
|
|
8
|
+
valid bits in the final packed byte, stored biased as 0-7 meaning 1-8.
|
|
9
|
+
|
|
10
|
+
There is no public specification -- [MS-XCA] scopes only the Xpress family -- so the
|
|
11
|
+
authority is the MIT-licensed ESE source: ``compression.cxx:985-1004`` (size math),
|
|
12
|
+
``:1168-1387`` / ``:1390-1504`` (ASCII/Unicode writers), ``:2113-2185`` /
|
|
13
|
+
``:2188-2272`` (ASCII/Unicode readers).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import TYPE_CHECKING, Final
|
|
20
|
+
|
|
21
|
+
from ntcompress.exceptions import DecompressionError
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from ntcompress.ese import Format
|
|
25
|
+
|
|
26
|
+
# --- Constants ---
|
|
27
|
+
|
|
28
|
+
UNIT_BITS: Final = 7
|
|
29
|
+
"""Packed bits per source unit; the writers' fixed ``cbits = 7`` (compression.cxx:1310)."""
|
|
30
|
+
|
|
31
|
+
UNIT_MASK: Final = 0x7F
|
|
32
|
+
"""Mask of one packed unit; both readers extract ``(x >> ibit) & 0x7F`` (compression.cxx:2166, 2243)."""
|
|
33
|
+
|
|
34
|
+
_BYTE_BITS: Final = 8
|
|
35
|
+
_WIDE_STRIDE: Final = 2 # bytes per UTF-16LE code unit (sizeof(WORD), compression.cxx:1002)
|
|
36
|
+
|
|
37
|
+
MIN_CELL_SIZE: Final = 2
|
|
38
|
+
"""Smallest well-formed cell: header byte + one packed byte, which the readers' ``Cb - 2`` size math presumes (compression.cxx:2135)."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# --- Shared framing ---
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class SevenBitHeader:
|
|
46
|
+
"""A parsed 7-bit cell header, per ``ErrDecompress7Bit*_`` (compression.cxx:2127-2137).
|
|
47
|
+
|
|
48
|
+
Captures the two facts the readers derive before unpacking: how many bits of
|
|
49
|
+
the last packed byte are valid, and hence exactly how many 7-bit units the
|
|
50
|
+
stream holds.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
fmt: Format
|
|
54
|
+
"""The format id from the header's top 5 bits (SEVEN_BIT_ASCII or SEVEN_BIT_UNICODE)."""
|
|
55
|
+
|
|
56
|
+
final_byte_bits: int
|
|
57
|
+
"""Valid bits in the last packed byte, 1-8: ``(bHeader & 0x7) + 1`` (compression.cxx:2131)."""
|
|
58
|
+
|
|
59
|
+
unit_count: int
|
|
60
|
+
"""Number of 7-bit units in the stream: ``((Cb - 2) * 8 + final_byte_bits) // 7`` (compression.cxx:2135-2137)."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _parse_header(blob: bytes, expected: Format) -> SevenBitHeader:
|
|
64
|
+
"""Validate a framed 7-bit cell and derive its unit count.
|
|
65
|
+
|
|
66
|
+
Ports the size prologue shared by both readers (compression.cxx:2127-2137).
|
|
67
|
+
ESE only *asserts* ``cbitTotal % 7 == 0`` in DEBUG builds and floor-divides in
|
|
68
|
+
retail; real-world producers do emit non-canonical bit counts (the same
|
|
69
|
+
Exchange payload circulates with header ``0x10`` and ``0x0e``), so this port
|
|
70
|
+
keeps the retail floor-division behavior.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
blob: The framed cell, header byte included.
|
|
74
|
+
expected: The format the calling codec owns.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
The parsed header.
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
DecompressionError: The cell is shorter than header + one stream byte, or
|
|
81
|
+
its format id is not ``expected``.
|
|
82
|
+
"""
|
|
83
|
+
from ntcompress.ese import format_flags, format_id
|
|
84
|
+
|
|
85
|
+
if len(blob) < MIN_CELL_SIZE:
|
|
86
|
+
msg = f"a 7-bit cell needs a header byte plus at least one packed byte, got {len(blob)} byte(s)"
|
|
87
|
+
raise DecompressionError(msg)
|
|
88
|
+
raw = format_id(blob[0])
|
|
89
|
+
if raw != expected:
|
|
90
|
+
msg = f"cell header carries format id 0x{raw:x}, not {expected.name} (0x{expected.value:x})"
|
|
91
|
+
raise DecompressionError(msg)
|
|
92
|
+
final_byte_bits = format_flags(blob[0]) + 1
|
|
93
|
+
total_bits = (len(blob) - 2) * _BYTE_BITS + final_byte_bits
|
|
94
|
+
return SevenBitHeader(fmt=expected, final_byte_bits=final_byte_bits, unit_count=total_bits // UNIT_BITS)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _check_padding(blob: bytes, header: SevenBitHeader) -> None:
|
|
98
|
+
"""Reject a cell whose declared-invalid final-byte bits are non-zero.
|
|
99
|
+
|
|
100
|
+
ESE's writers flush from a zero-initialized accumulator, so the padding bits
|
|
101
|
+
above ``final_byte_bits`` are always zero in a well-formed cell. This checks only
|
|
102
|
+
those high bits of the final packed byte; libesedb performs a comparable (not
|
|
103
|
+
identical) padding-rejection check on corrupt streams (cf. ``value_16bit != 0``,
|
|
104
|
+
libesedb_compression.c:220). Gated on ``verify`` by the callers.
|
|
105
|
+
"""
|
|
106
|
+
if blob[-1] >> header.final_byte_bits:
|
|
107
|
+
msg = f"non-zero padding above the {header.final_byte_bits} declared valid bit(s) of the final packed byte"
|
|
108
|
+
raise DecompressionError(msg)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _unpack_units(stream: bytes, count: int) -> bytearray:
|
|
112
|
+
"""Unpack ``count`` LSB-first 7-bit units from a packed stream.
|
|
113
|
+
|
|
114
|
+
Accumulator port of the readers' inner loop (compression.cxx:2154-2180): the C
|
|
115
|
+
code re-reads a byte or little-endian WORD at each bit offset, which is exactly
|
|
116
|
+
equivalent to draining 7-bit units off a little-endian accumulator. ``count``
|
|
117
|
+
comes from :func:`_parse_header`, which guarantees the stream holds enough bits.
|
|
118
|
+
"""
|
|
119
|
+
out = bytearray()
|
|
120
|
+
accumulator = 0
|
|
121
|
+
bits = 0
|
|
122
|
+
for byte in stream:
|
|
123
|
+
accumulator |= byte << bits
|
|
124
|
+
bits += _BYTE_BITS
|
|
125
|
+
while bits >= UNIT_BITS:
|
|
126
|
+
if len(out) == count:
|
|
127
|
+
return out
|
|
128
|
+
out.append(accumulator & UNIT_MASK)
|
|
129
|
+
accumulator >>= UNIT_BITS
|
|
130
|
+
bits -= UNIT_BITS
|
|
131
|
+
return out
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _pack_units(units: bytes) -> bytearray:
|
|
135
|
+
"""Pack 7-bit values LSB-first into a byte stream.
|
|
136
|
+
|
|
137
|
+
Accumulator port of the writers' slow path (compression.cxx:1298-1348 ASCII,
|
|
138
|
+
:1413-1464 Unicode): OR each unit's 7 bits at the current bit offset and flush
|
|
139
|
+
whole bytes. The final partial byte is always written, zero-padded high --
|
|
140
|
+
matching ESE's ``(ibitOutputCurr + 7) / 8`` trailing flush (:1354-1362).
|
|
141
|
+
"""
|
|
142
|
+
out = bytearray()
|
|
143
|
+
accumulator = 0
|
|
144
|
+
bits = 0
|
|
145
|
+
for value in units:
|
|
146
|
+
accumulator |= value << bits
|
|
147
|
+
bits += UNIT_BITS
|
|
148
|
+
if bits >= _BYTE_BITS:
|
|
149
|
+
out.append(accumulator & 0xFF)
|
|
150
|
+
accumulator >>= _BYTE_BITS
|
|
151
|
+
bits -= _BYTE_BITS
|
|
152
|
+
if bits:
|
|
153
|
+
out.append(accumulator)
|
|
154
|
+
return out
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _frame(fmt: Format, units: bytes) -> bytes:
|
|
158
|
+
"""Build a framed cell: header byte + packed stream.
|
|
159
|
+
|
|
160
|
+
The stored bit count is ``(ibitOutputCurr - 1) % 8`` (compression.cxx:1379,
|
|
161
|
+
:1497), which for ``n`` units reduces to ``(7n - 1) % 8`` -- the writers' 0->32
|
|
162
|
+
fixup for an exactly-full final DWORD (:1374-1378) folds into the same formula.
|
|
163
|
+
"""
|
|
164
|
+
from ntcompress.ese import header_byte
|
|
165
|
+
|
|
166
|
+
final_bits_biased = (len(units) * UNIT_BITS - 1) % _BYTE_BITS
|
|
167
|
+
return bytes([header_byte(fmt, final_bits_biased)]) + bytes(_pack_units(units))
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Checksums used by ESE compression headers.
|
|
2
|
+
|
|
3
|
+
Two non-stdlib CRCs are required, and both are implemented here from their literal
|
|
4
|
+
parameters. Python's :func:`zlib.crc32` uses a different polynomial and must not be
|
|
5
|
+
substituted for either.
|
|
6
|
+
|
|
7
|
+
* :func:`crc32c_ese` -- CRC-32C / Castagnoli (reflected polynomial ``0x82F63B78``,
|
|
8
|
+
normal form ``0x1EDC6F41``; init/xorout ``0xFFFFFFFF``): the "SSE 4.2 compatible" CRC
|
|
9
|
+
computed over the XPRESS10 plaintext. Verified check value
|
|
10
|
+
``crc32c_ese(b"123456789") == 0xE3069283``.
|
|
11
|
+
* :func:`crc64_ese` -- reflected polynomial ``0x9A6C9329AC4BC9B5``, init/xorout
|
|
12
|
+
all-ones: the "Corsica compatible" CRC computed over the XPRESS10 compressed
|
|
13
|
+
payload. Verified check value ``crc64_ese(b"123456789") == 0xAE8B14860A799888``.
|
|
14
|
+
This parameterization is **CRC-64/NVME** (RevEng catalogue: normal polynomial
|
|
15
|
+
``0xAD93D23594C93659``, reflected ``0x9A6C9329AC4BC9B5``, init/xorout all-ones,
|
|
16
|
+
refin/refout, check ``0xAE8B14860A799888``). It is NOT CRC-64/XZ (reflected poly
|
|
17
|
+
``0xC96C5795D7870F42``); do not use a library preset named ``crc64('xz')``.
|
|
18
|
+
|
|
19
|
+
Both are little-endian / reflected CRCs, so a right-shifting byte-wise table drives
|
|
20
|
+
them. The polynomials are already given in reflected form.
|
|
21
|
+
|
|
22
|
+
Authority: ``dev/ese/src/_xpress10/xpress10sw.cxx:34-59`` (the CRC-64 bit loop, poly
|
|
23
|
+
``CORSICA_CRC64_POLY``) and ESE ``Crc32Checksum`` in ``dev/ese/src/os/encrypt.cxx``
|
|
24
|
+
(CRC-32C).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from typing import TYPE_CHECKING, Final
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from ntcompress._types import Buffer
|
|
33
|
+
|
|
34
|
+
# --- Polynomials (reflected / LSB-first form) ---
|
|
35
|
+
|
|
36
|
+
CRC32C_POLY: Final = 0x82F63B78
|
|
37
|
+
"""Reflected CRC-32C (Castagnoli) polynomial (normal form 0x1EDC6F41)."""
|
|
38
|
+
|
|
39
|
+
CRC64_ESE_POLY: Final = 0x9A6C9329AC4BC9B5
|
|
40
|
+
"""Reflected CRC-64/NVME polynomial (xpress10sw.cxx:35, ``CORSICA_CRC64_POLY``). Not CRC-64/XZ."""
|
|
41
|
+
|
|
42
|
+
_MASK32: Final = 0xFFFFFFFF
|
|
43
|
+
_MASK64: Final = 0xFFFFFFFFFFFFFFFF
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _make_table(poly: int, mask: int) -> tuple[int, ...]:
|
|
47
|
+
"""Build a 256-entry byte-wise lookup table for a reflected (right-shifting) CRC."""
|
|
48
|
+
table: list[int] = []
|
|
49
|
+
for index in range(256):
|
|
50
|
+
crc = index
|
|
51
|
+
for _ in range(8):
|
|
52
|
+
crc = (crc >> 1) ^ poly if crc & 1 else crc >> 1
|
|
53
|
+
crc &= mask
|
|
54
|
+
table.append(crc)
|
|
55
|
+
return tuple(table)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
_CRC32C_TABLE: Final = _make_table(CRC32C_POLY, _MASK32)
|
|
59
|
+
_CRC64_TABLE: Final = _make_table(CRC64_ESE_POLY, _MASK64)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def crc32c_ese(data: Buffer) -> int:
|
|
63
|
+
"""Return the CRC-32C (Castagnoli) of ``data``, as ESE stores it in headers.
|
|
64
|
+
|
|
65
|
+
Reflected, init/xorout ``0xFFFFFFFF``. This is the "SSE 4.2 compatible" checksum
|
|
66
|
+
ESE computes over uncompressed data (``Crc32Checksum``); the XPRESS10 header
|
|
67
|
+
stores it over the plaintext.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
data: The bytes to checksum.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
The 32-bit CRC-32C. ``crc32c_ese(b"123456789") == 0xE3069283``.
|
|
74
|
+
"""
|
|
75
|
+
crc = _MASK32
|
|
76
|
+
for byte in data:
|
|
77
|
+
crc = (crc >> 8) ^ _CRC32C_TABLE[(crc ^ byte) & 0xFF]
|
|
78
|
+
return crc ^ _MASK32
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def crc64_ese(data: Buffer) -> int:
|
|
82
|
+
"""Return the ESE/Corsica CRC-64 of ``data``.
|
|
83
|
+
|
|
84
|
+
Reflected poly ``0x9A6C9329AC4BC9B5``, init/xorout all-ones -- the CRC-64/NVME
|
|
85
|
+
parameterization. The XPRESS10 header stores this over the compressed payload
|
|
86
|
+
(``UtilGenCorsicaCrc64``, ``xpress10sw.cxx:34-59``).
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
data: The bytes to checksum.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
The 64-bit CRC. ``crc64_ese(b"123456789") == 0xAE8B14860A799888``.
|
|
93
|
+
"""
|
|
94
|
+
crc = _MASK64
|
|
95
|
+
for byte in data:
|
|
96
|
+
crc = (crc >> 8) ^ _CRC64_TABLE[(crc ^ byte) & 0xFF]
|
|
97
|
+
return crc ^ _MASK64
|