-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathstream.py
More file actions
153 lines (130 loc) · 4.9 KB
/
Copy pathstream.py
File metadata and controls
153 lines (130 loc) · 4.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
"""Block-based streaming compression for large files and pipes.
tANS decodes a frame last-in-first-out, so a single frame can never be
decoded incrementally. The streaming layer instead splits the input into
independent blocks and emits one self-contained frame per block::
0..3 magic b"tANS"
4 format version (1)
5 mode: 2 = stream of blocks
per block: uvarint(frame byte length), frame (see pytans.frame)
terminator: a single zero length byte (0x00)
Memory use is bounded by the block size on both ends, each block carries
a table tuned to its own statistics, and each block is validated
independently on decode. :func:`decompress_stream` also accepts a plain
single-frame input, so it can decode anything :func:`pytans.compress`
produced.
"""
from __future__ import annotations
from typing import BinaryIO, Optional, Tuple
from .exceptions import CorruptedDataError
from .frame import (
MAGIC,
VERSION,
_MODE_RAW,
_MODE_STREAM,
_MODE_TANS,
_MODE_TRANSFORM,
_write_uvarint,
compress,
decompress,
)
from .tables import DEFAULT_MAX_TABLE_LOG
#: Default uncompressed block size (128 KiB). Large enough that the
#: per-block table header (<1 KiB worst case) is noise, small enough to
#: keep memory use and decode latency low.
DEFAULT_BLOCK_SIZE = 128 * 1024
#: Upper bound accepted for a single block frame when decoding, guarding
#: against absurd lengths in corrupted streams.
_MAX_BLOCK_FRAME = 1 << 30
def _read_up_to(src: BinaryIO, n: int) -> bytes:
"""Read up to ``n`` bytes, looping over short reads (pipes, sockets)."""
buf = bytearray()
while len(buf) < n:
chunk = src.read(n - len(buf))
if not chunk:
break
buf += chunk
return bytes(buf)
def _read_exact(src: BinaryIO, n: int) -> bytes:
data = _read_up_to(src, n)
if len(data) != n:
raise CorruptedDataError("truncated stream")
return data
def _read_uvarint(src: BinaryIO) -> int:
value = 0
shift = 0
while True:
byte = _read_exact(src, 1)[0]
value |= (byte & 0x7F) << shift
if not byte & 0x80:
return value
shift += 7
if shift > 63:
raise CorruptedDataError("invalid length in stream")
def compress_stream(
src: BinaryIO,
dst: BinaryIO,
*,
block_size: int = DEFAULT_BLOCK_SIZE,
table_log: Optional[int] = None,
max_table_log: int = DEFAULT_MAX_TABLE_LOG,
transform: Optional[str] = None,
) -> Tuple[int, int]:
"""Compress ``src`` into ``dst`` block by block.
``src``/``dst`` are binary file-like objects (open files, pipes,
``io.BytesIO`` ...). ``transform`` enables a per-block modeling stage
(``"bwt"`` or ``"lz77"``) for repetitive data. Returns
``(bytes_read, bytes_written)``.
"""
if block_size < 1:
raise ValueError("block_size must be at least 1")
dst.write(MAGIC + bytes((VERSION, _MODE_STREAM)))
bytes_read, bytes_written = 0, 6
while True:
block = _read_up_to(src, block_size)
if not block:
break
frame = compress(block, table_log, max_table_log, transform=transform)
dst.write(_write_uvarint(len(frame)) + frame)
bytes_read += len(block)
bytes_written += len(_write_uvarint(len(frame))) + len(frame)
dst.write(b"\x00")
return bytes_read, bytes_written + 1
def decompress_stream(src: BinaryIO, dst: BinaryIO) -> Tuple[int, int]:
"""Decompress a stream (or a single frame) from ``src`` into ``dst``.
Returns ``(bytes_read, bytes_written)``. Raises
:class:`CorruptedDataError` on truncated, malformed or tampered input.
"""
header = _read_exact(src, 6)
if header[:4] != MAGIC:
raise CorruptedDataError("not a pytans stream (bad magic)")
if header[4] != VERSION:
raise CorruptedDataError(f"unsupported version {header[4]}")
mode = header[5]
if mode in (_MODE_RAW, _MODE_TANS, _MODE_TRANSFORM):
# A one-shot pytans.compress() frame: by definition single-block.
body = bytearray(header)
while True:
chunk = src.read(DEFAULT_BLOCK_SIZE)
if not chunk:
break
body += chunk
data = decompress(bytes(body))
dst.write(data)
return len(body), len(data)
if mode != _MODE_STREAM:
raise CorruptedDataError(f"unknown stream mode {mode}")
bytes_read, bytes_written = 6, 0
while True:
frame_len = _read_uvarint(src)
bytes_read += len(_write_uvarint(frame_len))
if frame_len == 0:
break
if frame_len > _MAX_BLOCK_FRAME:
raise CorruptedDataError("block frame length out of range")
block = decompress(_read_exact(src, frame_len))
dst.write(block)
bytes_read += frame_len
bytes_written += len(block)
if src.read(1):
raise CorruptedDataError("trailing data after stream terminator")
return bytes_read, bytes_written