hashrope 0.1.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.
- hashrope/__init__.py +64 -0
- hashrope/polynomial_hash.py +241 -0
- hashrope/rope.py +601 -0
- hashrope/sliding.py +210 -0
- hashrope-0.1.0.dist-info/METADATA +191 -0
- hashrope-0.1.0.dist-info/RECORD +8 -0
- hashrope-0.1.0.dist-info/WHEEL +4 -0
- hashrope-0.1.0.dist-info/licenses/LICENSE +21 -0
hashrope/__init__.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
hashrope -- Hash rope data structure with algebraic content addressing.
|
|
3
|
+
|
|
4
|
+
A balanced binary tree (BB[2/7]) augmented with polynomial hash metadata
|
|
5
|
+
at every node. Supports O(log w) concat, split, and substring hashing,
|
|
6
|
+
plus O(log q) repetition encoding via RepeatNode and geometric accumulator.
|
|
7
|
+
|
|
8
|
+
All arithmetic is performed modulo a Mersenne prime using bit-shift
|
|
9
|
+
reduction (no expensive division).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
__version__ = "0.1.0"
|
|
13
|
+
|
|
14
|
+
from hashrope.polynomial_hash import (
|
|
15
|
+
PolynomialHash,
|
|
16
|
+
phi,
|
|
17
|
+
mersenne_mod,
|
|
18
|
+
mersenne_mul,
|
|
19
|
+
MERSENNE_61,
|
|
20
|
+
MERSENNE_127,
|
|
21
|
+
)
|
|
22
|
+
from hashrope.rope import (
|
|
23
|
+
Leaf,
|
|
24
|
+
Internal,
|
|
25
|
+
RepeatNode,
|
|
26
|
+
Node,
|
|
27
|
+
rope_concat,
|
|
28
|
+
rope_split,
|
|
29
|
+
rope_repeat,
|
|
30
|
+
rope_substr_hash,
|
|
31
|
+
rope_len,
|
|
32
|
+
rope_hash,
|
|
33
|
+
rope_from_bytes,
|
|
34
|
+
rope_to_bytes,
|
|
35
|
+
validate_rope,
|
|
36
|
+
)
|
|
37
|
+
from hashrope.sliding import SlidingWindow
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
# Polynomial hash
|
|
41
|
+
"PolynomialHash",
|
|
42
|
+
"phi",
|
|
43
|
+
"mersenne_mod",
|
|
44
|
+
"mersenne_mul",
|
|
45
|
+
"MERSENNE_61",
|
|
46
|
+
"MERSENNE_127",
|
|
47
|
+
# Rope nodes
|
|
48
|
+
"Leaf",
|
|
49
|
+
"Internal",
|
|
50
|
+
"RepeatNode",
|
|
51
|
+
"Node",
|
|
52
|
+
# Rope operations
|
|
53
|
+
"rope_concat",
|
|
54
|
+
"rope_split",
|
|
55
|
+
"rope_repeat",
|
|
56
|
+
"rope_substr_hash",
|
|
57
|
+
"rope_len",
|
|
58
|
+
"rope_hash",
|
|
59
|
+
"rope_from_bytes",
|
|
60
|
+
"rope_to_bytes",
|
|
61
|
+
"validate_rope",
|
|
62
|
+
# Sliding window
|
|
63
|
+
"SlidingWindow",
|
|
64
|
+
]
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Polynomial hash over Z/pZ with Mersenne prime arithmetic.
|
|
3
|
+
|
|
4
|
+
Implements Definitions 1-3, Lemmas 1-2, 13-14, Theorems 1-3, 5
|
|
5
|
+
from the compressed-domain hashing framework.
|
|
6
|
+
|
|
7
|
+
All arithmetic is performed modulo a Mersenne prime p = 2^61 - 1
|
|
8
|
+
using bit-shift reduction (Lemma 14) to avoid expensive division.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
# Constants
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
MERSENNE_61: int = (1 << 61) - 1 # 2^61 - 1
|
|
18
|
+
MERSENNE_127: int = (1 << 127) - 1 # 2^127 - 1 (recommended for high security, Corollary 5)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Mersenne prime arithmetic (Lemma 14)
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
def mersenne_mod(a: int, p: int = MERSENNE_61) -> int:
|
|
26
|
+
"""
|
|
27
|
+
Reduce a non-negative integer a modulo a Mersenne prime p = 2^k - 1.
|
|
28
|
+
|
|
29
|
+
Uses the identity: a mod (2^k - 1) = (a >> k) + (a & ((1 << k) - 1))
|
|
30
|
+
with at most one conditional subtraction.
|
|
31
|
+
|
|
32
|
+
Proof (Lemma 14): Write a = q·2^k + r. Then a = q·(p+1) + r = q·p + (q+r),
|
|
33
|
+
so a ≡ q + r (mod p). Since q + r < 2^(k+1), at most one subtraction suffices.
|
|
34
|
+
"""
|
|
35
|
+
# Determine k from p: p = 2^k - 1, so k = p.bit_length()
|
|
36
|
+
k = p.bit_length()
|
|
37
|
+
# Repeated folding for very large values
|
|
38
|
+
while a >= (1 << (2 * k)):
|
|
39
|
+
a = (a >> k) + (a & p)
|
|
40
|
+
# Final fold
|
|
41
|
+
a = (a >> k) + (a & p)
|
|
42
|
+
# At most one more fold needed
|
|
43
|
+
a = (a >> k) + (a & p)
|
|
44
|
+
# Conditional subtraction
|
|
45
|
+
if a >= p:
|
|
46
|
+
a -= p
|
|
47
|
+
return a
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def mersenne_mul(a: int, b: int, p: int = MERSENNE_61) -> int:
|
|
51
|
+
"""
|
|
52
|
+
Compute (a * b) mod p for Mersenne prime p.
|
|
53
|
+
|
|
54
|
+
Python's arbitrary-precision integers make this straightforward:
|
|
55
|
+
multiply natively, then reduce via mersenne_mod.
|
|
56
|
+
"""
|
|
57
|
+
return mersenne_mod(a * b, p)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# Geometric accumulator Φ (Definition 3, Theorem 3)
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
def phi(q: int, alpha: int, p: int = MERSENNE_61) -> int:
|
|
65
|
+
"""
|
|
66
|
+
Compute Φ(q, α) = Σ_{i=0}^{q-1} α^i (mod p) via inverse-free
|
|
67
|
+
repeated doubling.
|
|
68
|
+
|
|
69
|
+
Recurrence (Theorem 3):
|
|
70
|
+
Φ(0, α) = 0
|
|
71
|
+
Φ(1, α) = 1
|
|
72
|
+
Φ(2k, α) = Φ(k, α) · (1 + α^k)
|
|
73
|
+
Φ(2k+1, α) = Φ(2k, α) · α + 1
|
|
74
|
+
|
|
75
|
+
Handles the degenerate case α ≡ 1 (mod p) correctly,
|
|
76
|
+
yielding Φ(q, 1) = q (mod p) (Lemma 3).
|
|
77
|
+
|
|
78
|
+
Time: O(log q) multiplications in Z/pZ.
|
|
79
|
+
|
|
80
|
+
Implementation note: We scan q's bits left-to-right (MSB to LSB).
|
|
81
|
+
We maintain alpha_power = α^n where n is the current accumulated
|
|
82
|
+
value of q built so far. At each step:
|
|
83
|
+
- Doubling (n → 2n): alpha_power squares (α^n → α^(2n))
|
|
84
|
+
- Odd step (n → n+1): alpha_power multiplies by α (α^n → α^(n+1))
|
|
85
|
+
This ensures the correct α^k is used in each doubling step.
|
|
86
|
+
"""
|
|
87
|
+
if q == 0:
|
|
88
|
+
return 0
|
|
89
|
+
if q == 1:
|
|
90
|
+
return 1
|
|
91
|
+
|
|
92
|
+
bits = q.bit_length()
|
|
93
|
+
|
|
94
|
+
phi_val = 1 # Φ(1, α) = 1
|
|
95
|
+
alpha_power = alpha # α^1 — tracks α^(current n)
|
|
96
|
+
|
|
97
|
+
# Scan from the second-most-significant bit down to bit 0
|
|
98
|
+
for i in range(bits - 2, -1, -1):
|
|
99
|
+
# Even step: Φ(2k, α) = Φ(k, α) · (1 + α^k)
|
|
100
|
+
# alpha_power currently holds α^k
|
|
101
|
+
phi_val = mersenne_mul(phi_val, (1 + alpha_power) % p, p)
|
|
102
|
+
# α^k → α^(2k)
|
|
103
|
+
alpha_power = mersenne_mul(alpha_power, alpha_power, p)
|
|
104
|
+
|
|
105
|
+
# If current bit is 1: odd step (2k → 2k+1)
|
|
106
|
+
if (q >> i) & 1:
|
|
107
|
+
# Φ(2k+1, α) = Φ(2k, α) · α + 1
|
|
108
|
+
phi_val = (mersenne_mul(phi_val, alpha, p) + 1) % p
|
|
109
|
+
# α^(2k) → α^(2k+1)
|
|
110
|
+
alpha_power = mersenne_mul(alpha_power, alpha, p)
|
|
111
|
+
|
|
112
|
+
return phi_val
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
# Polynomial Hash (Definition 2, Theorems 1-2, 5)
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
class PolynomialHash:
|
|
120
|
+
"""
|
|
121
|
+
Polynomial hash function over Z/pZ.
|
|
122
|
+
|
|
123
|
+
H(s) = Σ_{i=0}^{n-1} (s_i + 1) · x^(n-1-i) (mod p)
|
|
124
|
+
|
|
125
|
+
The +1 offset (Definition 2) ensures no byte maps to zero in Z/pZ,
|
|
126
|
+
preventing the zero-padding collision (Lemma 1).
|
|
127
|
+
|
|
128
|
+
Parameters
|
|
129
|
+
----------
|
|
130
|
+
prime : int
|
|
131
|
+
A Mersenne prime. Default: 2^61 - 1.
|
|
132
|
+
base : int
|
|
133
|
+
The hash base x, chosen from {2, ..., p-1}.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
__slots__ = ("_p", "_x", "_power_cache")
|
|
137
|
+
|
|
138
|
+
def __init__(self, prime: int = MERSENNE_61, base: int = 131) -> None:
|
|
139
|
+
if base < 2 or base >= prime:
|
|
140
|
+
raise ValueError(f"Base must be in [2, p-1], got {base}")
|
|
141
|
+
self._p = prime
|
|
142
|
+
self._x = base
|
|
143
|
+
self._power_cache: dict[int, int] = {0: 1, 1: base}
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def prime(self) -> int:
|
|
147
|
+
return self._p
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def base(self) -> int:
|
|
151
|
+
return self._x
|
|
152
|
+
|
|
153
|
+
def power(self, n: int) -> int:
|
|
154
|
+
"""
|
|
155
|
+
Compute x^n mod p with caching.
|
|
156
|
+
|
|
157
|
+
Uses Python's built-in pow(x, n, p) which implements
|
|
158
|
+
binary exponentiation (Lemma 13).
|
|
159
|
+
"""
|
|
160
|
+
if n in self._power_cache:
|
|
161
|
+
return self._power_cache[n]
|
|
162
|
+
result = pow(self._x, n, self._p)
|
|
163
|
+
self._power_cache[n] = result
|
|
164
|
+
return result
|
|
165
|
+
|
|
166
|
+
def hash(self, data: bytes) -> int:
|
|
167
|
+
"""
|
|
168
|
+
Compute H(data) = Σ (data[i] + 1) · x^(n-1-i) (mod p).
|
|
169
|
+
|
|
170
|
+
For the empty string, returns 0 (Definition 2).
|
|
171
|
+
|
|
172
|
+
Parameters
|
|
173
|
+
----------
|
|
174
|
+
data : bytes
|
|
175
|
+
The byte string to hash.
|
|
176
|
+
|
|
177
|
+
Returns
|
|
178
|
+
-------
|
|
179
|
+
int
|
|
180
|
+
Hash value in [0, p-1].
|
|
181
|
+
"""
|
|
182
|
+
if len(data) == 0:
|
|
183
|
+
return 0
|
|
184
|
+
|
|
185
|
+
p = self._p
|
|
186
|
+
x = self._x
|
|
187
|
+
h = 0
|
|
188
|
+
for byte in data:
|
|
189
|
+
# h = h · x + (byte + 1)
|
|
190
|
+
h = mersenne_mod(h * x + byte + 1, p)
|
|
191
|
+
return h
|
|
192
|
+
|
|
193
|
+
def hash_concat(self, h_a: int, len_b: int, h_b: int) -> int:
|
|
194
|
+
"""
|
|
195
|
+
Compute H(A ‖ B) from H(A), |B|, H(B) via Theorem 1.
|
|
196
|
+
|
|
197
|
+
H(A ‖ B) = H(A) · x^|B| + H(B) (mod p)
|
|
198
|
+
"""
|
|
199
|
+
x_pow = self.power(len_b)
|
|
200
|
+
return mersenne_mod(h_a * x_pow + h_b, self._p)
|
|
201
|
+
|
|
202
|
+
def hash_repeat(self, h_s: int, d: int, q: int) -> int:
|
|
203
|
+
"""
|
|
204
|
+
Compute H(S^q) from H(S) and |S| = d via Theorem 2.
|
|
205
|
+
|
|
206
|
+
H(S^q) = H(S) · Φ(q, x^d) (mod p)
|
|
207
|
+
"""
|
|
208
|
+
x_d = self.power(d)
|
|
209
|
+
phi_val = phi(q, x_d, self._p)
|
|
210
|
+
return mersenne_mul(h_s, phi_val, self._p)
|
|
211
|
+
|
|
212
|
+
def hash_overlap(self, h_p: int, d: int, l: int, h_prefix: int) -> int:
|
|
213
|
+
"""
|
|
214
|
+
Compute H(W) for overlapping back-reference via Theorem 5.
|
|
215
|
+
|
|
216
|
+
W = P^q ‖ P[0..r-1] where q = ⌊l/d⌋, r = l mod d.
|
|
217
|
+
|
|
218
|
+
H(W) = H(P) · Φ(q, x^d) · x^r + H(P[0..r-1]) (mod p)
|
|
219
|
+
|
|
220
|
+
Parameters
|
|
221
|
+
----------
|
|
222
|
+
h_p : int
|
|
223
|
+
H(P), hash of the pattern of length d.
|
|
224
|
+
d : int
|
|
225
|
+
Pattern length.
|
|
226
|
+
l : int
|
|
227
|
+
Total back-reference length (d < l for overlapping).
|
|
228
|
+
h_prefix : int
|
|
229
|
+
H(P[0..r-1]) where r = l mod d. Pass 0 if r = 0.
|
|
230
|
+
"""
|
|
231
|
+
q, r = divmod(l, d)
|
|
232
|
+
x_d = self.power(d)
|
|
233
|
+
x_r = self.power(r)
|
|
234
|
+
|
|
235
|
+
# H(P) · Φ(q, x^d)
|
|
236
|
+
phi_val = phi(q, x_d, self._p)
|
|
237
|
+
h_repeated = mersenne_mul(h_p, phi_val, self._p)
|
|
238
|
+
|
|
239
|
+
# · x^r + H(P[0..r-1])
|
|
240
|
+
result = mersenne_mod(h_repeated * x_r + h_prefix, self._p)
|
|
241
|
+
return result
|
hashrope/rope.py
ADDED
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hash rope data structure for compressed-domain hashing.
|
|
3
|
+
|
|
4
|
+
Implements Part III of the framework:
|
|
5
|
+
- Definition 6: Leaf, Internal, RepeatNode
|
|
6
|
+
- Definition 7: Invariants I1-I9 (BB[2/7] weight balance)
|
|
7
|
+
- Lemma 4: Hash correctness (structural induction)
|
|
8
|
+
- Lemma 5: Height bound O(log w)
|
|
9
|
+
- Lemma 6: Rotations preserve hash invariant
|
|
10
|
+
- Lemma 7: RepeatNode splittability
|
|
11
|
+
- Lemma 8: Join complexity O(k · |h_L - h_R|)
|
|
12
|
+
- Lemma 8': RepeatNode bisection for rebalancing
|
|
13
|
+
- Theorem 6: Join (full specification)
|
|
14
|
+
- Theorem 7: Split in O(k · log w)
|
|
15
|
+
- Theorem 8: Repeat via RepeatNode in O(k · log q), O(1) space
|
|
16
|
+
- Theorem 9: Allocation-free SubstrHash in O(k · log w)
|
|
17
|
+
- Theorem 10: Concat = Join
|
|
18
|
+
|
|
19
|
+
All nodes are immutable (Invariant I9). Operations return new trees
|
|
20
|
+
with structural sharing.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from typing import Optional
|
|
27
|
+
|
|
28
|
+
from hashrope.polynomial_hash import (
|
|
29
|
+
PolynomialHash,
|
|
30
|
+
phi,
|
|
31
|
+
mersenne_mod,
|
|
32
|
+
mersenne_mul,
|
|
33
|
+
MERSENNE_61,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# Balance parameter (Definition 7, Invariant I8)
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
# BB[α] with α = 2/7 (Nievergelt-Reingold condition).
|
|
41
|
+
# A node is balanced if each child's weight is at least α * parent weight.
|
|
42
|
+
ALPHA_NUM = 2
|
|
43
|
+
ALPHA_DEN = 7
|
|
44
|
+
|
|
45
|
+
# Type alias
|
|
46
|
+
Node = Optional["Leaf | Internal | RepeatNode"]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# Definition 6: Node types
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True, slots=True)
|
|
55
|
+
class Leaf:
|
|
56
|
+
"""
|
|
57
|
+
Leaf node storing a byte sequence (Definition 6a).
|
|
58
|
+
|
|
59
|
+
Invariant I1: hash_val = H(data).
|
|
60
|
+
Weight is always 1.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
data: bytes
|
|
64
|
+
hash_val: int
|
|
65
|
+
len: int
|
|
66
|
+
weight: int
|
|
67
|
+
|
|
68
|
+
def __init__(self, data: bytes, h: PolynomialHash) -> None:
|
|
69
|
+
if len(data) == 0:
|
|
70
|
+
raise ValueError("Leaf cannot be empty")
|
|
71
|
+
object.__setattr__(self, "data", bytes(data))
|
|
72
|
+
object.__setattr__(self, "len", len(data))
|
|
73
|
+
object.__setattr__(self, "hash_val", h.hash(data))
|
|
74
|
+
object.__setattr__(self, "weight", 1)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True, slots=True)
|
|
78
|
+
class Internal:
|
|
79
|
+
"""
|
|
80
|
+
Internal node joining two subtrees (Definition 6b).
|
|
81
|
+
|
|
82
|
+
Invariant I2: len = left.len + right.len
|
|
83
|
+
Invariant I3: hash_val = left.hash_val · x^(right.len) + right.hash_val
|
|
84
|
+
Invariant I4: weight = left.weight + right.weight
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
left: Leaf | Internal | RepeatNode
|
|
88
|
+
right: Leaf | Internal | RepeatNode
|
|
89
|
+
hash_val: int
|
|
90
|
+
len: int
|
|
91
|
+
weight: int
|
|
92
|
+
|
|
93
|
+
def __init__(
|
|
94
|
+
self,
|
|
95
|
+
left: Leaf | Internal | RepeatNode,
|
|
96
|
+
right: Leaf | Internal | RepeatNode,
|
|
97
|
+
h: PolynomialHash,
|
|
98
|
+
) -> None:
|
|
99
|
+
object.__setattr__(self, "left", left)
|
|
100
|
+
object.__setattr__(self, "right", right)
|
|
101
|
+
object.__setattr__(self, "len", left.len + right.len)
|
|
102
|
+
object.__setattr__(self, "weight", left.weight + right.weight)
|
|
103
|
+
# I3: hash = left.hash · x^(right.len) + right.hash
|
|
104
|
+
hv = h.hash_concat(left.hash_val, right.len, right.hash_val)
|
|
105
|
+
object.__setattr__(self, "hash_val", hv)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass(frozen=True, slots=True)
|
|
109
|
+
class RepeatNode:
|
|
110
|
+
"""
|
|
111
|
+
Repeat node representing child^reps (Definition 6c).
|
|
112
|
+
|
|
113
|
+
Invariant I5: len = child.len * reps
|
|
114
|
+
Invariant I6: hash_val = child.hash_val · Φ(reps, x^(child.len))
|
|
115
|
+
Invariant I7: weight = child.weight * reps
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
child: Leaf | Internal | RepeatNode
|
|
119
|
+
reps: int
|
|
120
|
+
hash_val: int
|
|
121
|
+
len: int
|
|
122
|
+
weight: int
|
|
123
|
+
|
|
124
|
+
def __init__(
|
|
125
|
+
self,
|
|
126
|
+
child: Leaf | Internal | RepeatNode,
|
|
127
|
+
reps: int,
|
|
128
|
+
h: PolynomialHash,
|
|
129
|
+
) -> None:
|
|
130
|
+
if reps < 2:
|
|
131
|
+
raise ValueError(f"RepeatNode reps must be >= 2, got {reps}")
|
|
132
|
+
object.__setattr__(self, "child", child)
|
|
133
|
+
object.__setattr__(self, "reps", reps)
|
|
134
|
+
object.__setattr__(self, "len", child.len * reps)
|
|
135
|
+
object.__setattr__(self, "weight", child.weight * reps)
|
|
136
|
+
# I6: hash = child.hash · Φ(reps, x^d)
|
|
137
|
+
x_d = h.power(child.len)
|
|
138
|
+
phi_val = phi(reps, x_d, h.prime)
|
|
139
|
+
hv = mersenne_mul(child.hash_val, phi_val, h.prime)
|
|
140
|
+
object.__setattr__(self, "hash_val", hv)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# ---------------------------------------------------------------------------
|
|
144
|
+
# Accessors
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def rope_len(node: Node) -> int:
|
|
149
|
+
"""Total byte length of the string represented by node."""
|
|
150
|
+
return 0 if node is None else node.len
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def rope_hash(node: Node) -> int:
|
|
154
|
+
"""Hash of the string represented by node."""
|
|
155
|
+
return 0 if node is None else node.hash_val
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _weight(node: Node) -> int:
|
|
159
|
+
return 0 if node is None else node.weight
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _height(node: Node) -> int:
|
|
163
|
+
"""Approximate height for join decisions."""
|
|
164
|
+
if node is None:
|
|
165
|
+
return 0
|
|
166
|
+
if isinstance(node, Leaf):
|
|
167
|
+
return 1
|
|
168
|
+
if isinstance(node, RepeatNode):
|
|
169
|
+
return _height(node.child) + 1
|
|
170
|
+
# Internal
|
|
171
|
+
return 1 + max(_height(node.left), _height(node.right))
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# ---------------------------------------------------------------------------
|
|
175
|
+
# Balance helpers (Invariant I8)
|
|
176
|
+
# ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _is_balanced(left: Node, right: Node) -> bool:
|
|
180
|
+
"""Check BB[2/7] condition for a potential Internal(left, right)."""
|
|
181
|
+
total = _weight(left) + _weight(right)
|
|
182
|
+
if total <= 2:
|
|
183
|
+
return True
|
|
184
|
+
wl = _weight(left)
|
|
185
|
+
# α ≤ wl/total ≤ 1-α ⟺ α·total ≤ wl ≤ (1-α)·total
|
|
186
|
+
# With α = 2/7: 2·total ≤ 7·wl and 7·wl ≤ 5·total
|
|
187
|
+
return ALPHA_NUM * total <= ALPHA_DEN * wl <= (ALPHA_DEN - ALPHA_NUM) * total
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _rotate_right(node: Internal, h: PolynomialHash) -> Leaf | Internal | RepeatNode:
|
|
191
|
+
"""Right rotation: Internal(Internal(A, B), C) → Internal(A, Internal(B, C))."""
|
|
192
|
+
u = node.left
|
|
193
|
+
if not isinstance(u, Internal):
|
|
194
|
+
return node # can't rotate
|
|
195
|
+
a, b, c = u.left, u.right, node.right
|
|
196
|
+
new_right = Internal(b, c, h)
|
|
197
|
+
return Internal(a, new_right, h)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _rotate_left(node: Internal, h: PolynomialHash) -> Leaf | Internal | RepeatNode:
|
|
201
|
+
"""Left rotation: Internal(A, Internal(B, C)) → Internal(Internal(A, B), C)."""
|
|
202
|
+
u = node.right
|
|
203
|
+
if not isinstance(u, Internal):
|
|
204
|
+
return node
|
|
205
|
+
a, b, c = node.left, u.left, u.right
|
|
206
|
+
new_left = Internal(a, b, h)
|
|
207
|
+
return Internal(new_left, c, h)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _rebalance(left: Leaf | Internal | RepeatNode,
|
|
211
|
+
right: Leaf | Internal | RepeatNode,
|
|
212
|
+
h: PolynomialHash) -> Leaf | Internal | RepeatNode:
|
|
213
|
+
"""
|
|
214
|
+
Create a balanced Internal node from left and right.
|
|
215
|
+
Applies single or double rotations as needed (Lemma 6).
|
|
216
|
+
"""
|
|
217
|
+
node = Internal(left, right, h)
|
|
218
|
+
wl, wr = _weight(left), _weight(right)
|
|
219
|
+
total = wl + wr
|
|
220
|
+
|
|
221
|
+
if total <= 2 or _is_balanced(left, right):
|
|
222
|
+
return node
|
|
223
|
+
|
|
224
|
+
if wl * ALPHA_DEN < ALPHA_NUM * total:
|
|
225
|
+
# Left too light → rotate left
|
|
226
|
+
if isinstance(right, Internal):
|
|
227
|
+
# Check if double rotation needed
|
|
228
|
+
if _weight(right.left) * ALPHA_DEN > (ALPHA_DEN - ALPHA_NUM) * wr:
|
|
229
|
+
# Double: rotate right child right, then rotate left
|
|
230
|
+
new_right = _rotate_right(right, h)
|
|
231
|
+
node = Internal(left, new_right, h)
|
|
232
|
+
return _rotate_left(node, h)
|
|
233
|
+
return node
|
|
234
|
+
|
|
235
|
+
if wr * ALPHA_DEN < ALPHA_NUM * total:
|
|
236
|
+
# Right too light → rotate right
|
|
237
|
+
if isinstance(left, Internal):
|
|
238
|
+
if _weight(left.right) * ALPHA_DEN > (ALPHA_DEN - ALPHA_NUM) * wl:
|
|
239
|
+
new_left = _rotate_left(left, h)
|
|
240
|
+
node = Internal(new_left, right, h)
|
|
241
|
+
return _rotate_right(node, h)
|
|
242
|
+
return node
|
|
243
|
+
|
|
244
|
+
return node
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# ---------------------------------------------------------------------------
|
|
248
|
+
# Theorem 6 / Lemma 8: Join (Concat)
|
|
249
|
+
# ---------------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def rope_concat(
|
|
253
|
+
left: Node, right: Node, h: PolynomialHash
|
|
254
|
+
) -> Node:
|
|
255
|
+
"""
|
|
256
|
+
Join two ropes (Theorem 6, Theorem 10).
|
|
257
|
+
|
|
258
|
+
Returns a valid hash rope representing left's string ‖ right's string.
|
|
259
|
+
Time: O(k · |h_L - h_R|).
|
|
260
|
+
"""
|
|
261
|
+
if left is None:
|
|
262
|
+
return right
|
|
263
|
+
if right is None:
|
|
264
|
+
return left
|
|
265
|
+
return _join(left, right, h)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _join(
|
|
269
|
+
left: Leaf | Internal | RepeatNode,
|
|
270
|
+
right: Leaf | Internal | RepeatNode,
|
|
271
|
+
h: PolynomialHash,
|
|
272
|
+
) -> Leaf | Internal | RepeatNode:
|
|
273
|
+
"""Core join with rebalancing."""
|
|
274
|
+
if _is_balanced(left, right):
|
|
275
|
+
return Internal(left, right, h)
|
|
276
|
+
|
|
277
|
+
wl, wr = _weight(left), _weight(right)
|
|
278
|
+
|
|
279
|
+
if wl > wr:
|
|
280
|
+
# Left is heavier — descend right spine of left
|
|
281
|
+
if isinstance(left, Internal):
|
|
282
|
+
new_right = _join(left.right, right, h)
|
|
283
|
+
return _rebalance(left.left, new_right, h)
|
|
284
|
+
elif isinstance(left, RepeatNode):
|
|
285
|
+
# Split the repeat node in half, join right half with right
|
|
286
|
+
mid = left.len // 2
|
|
287
|
+
ll, lr = rope_split(left, mid, h)
|
|
288
|
+
new_right = _join(lr, right, h)
|
|
289
|
+
return _join(ll, new_right, h)
|
|
290
|
+
else:
|
|
291
|
+
# Leaf — just make Internal
|
|
292
|
+
return Internal(left, right, h)
|
|
293
|
+
else:
|
|
294
|
+
# Right is heavier — descend left spine of right
|
|
295
|
+
if isinstance(right, Internal):
|
|
296
|
+
new_left = _join(left, right.left, h)
|
|
297
|
+
return _rebalance(new_left, right.right, h)
|
|
298
|
+
elif isinstance(right, RepeatNode):
|
|
299
|
+
mid = right.len // 2
|
|
300
|
+
rl, rr = rope_split(right, mid, h)
|
|
301
|
+
new_left = _join(left, rl, h)
|
|
302
|
+
return _join(new_left, rr, h)
|
|
303
|
+
else:
|
|
304
|
+
return Internal(left, right, h)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# ---------------------------------------------------------------------------
|
|
308
|
+
# Theorem 7: Split
|
|
309
|
+
# ---------------------------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def rope_split(
|
|
313
|
+
node: Node, pos: int, h: PolynomialHash
|
|
314
|
+
) -> tuple[Node, Node]:
|
|
315
|
+
"""
|
|
316
|
+
Split rope at byte position pos (Theorem 7).
|
|
317
|
+
|
|
318
|
+
Returns (left, right) where left has pos bytes, right has the rest.
|
|
319
|
+
Time: O(k · log w).
|
|
320
|
+
"""
|
|
321
|
+
if node is None:
|
|
322
|
+
return None, None
|
|
323
|
+
if pos <= 0:
|
|
324
|
+
return None, node
|
|
325
|
+
if pos >= node.len:
|
|
326
|
+
return node, None
|
|
327
|
+
return _split(node, pos, h)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _split(
|
|
331
|
+
node: Leaf | Internal | RepeatNode,
|
|
332
|
+
pos: int,
|
|
333
|
+
h: PolynomialHash,
|
|
334
|
+
) -> tuple[Node, Node]:
|
|
335
|
+
"""Core split. 0 < pos < node.len guaranteed."""
|
|
336
|
+
|
|
337
|
+
if isinstance(node, Leaf):
|
|
338
|
+
left = Leaf(node.data[:pos], h)
|
|
339
|
+
right = Leaf(node.data[pos:], h)
|
|
340
|
+
return left, right
|
|
341
|
+
|
|
342
|
+
if isinstance(node, Internal):
|
|
343
|
+
ll = node.left.len
|
|
344
|
+
if pos == ll:
|
|
345
|
+
return node.left, node.right
|
|
346
|
+
if pos < ll:
|
|
347
|
+
l1, l2 = _split(node.left, pos, h)
|
|
348
|
+
return l1, rope_concat(l2, node.right, h)
|
|
349
|
+
else:
|
|
350
|
+
r1, r2 = _split(node.right, pos - ll, h)
|
|
351
|
+
return rope_concat(node.left, r1, h), r2
|
|
352
|
+
|
|
353
|
+
if isinstance(node, RepeatNode):
|
|
354
|
+
return _split_repeat(node, pos, h)
|
|
355
|
+
|
|
356
|
+
raise TypeError(f"Unknown node type: {type(node)}")
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _split_repeat(
|
|
360
|
+
node: RepeatNode, pos: int, h: PolynomialHash
|
|
361
|
+
) -> tuple[Node, Node]:
|
|
362
|
+
"""
|
|
363
|
+
Split a RepeatNode at byte position pos (Lemma 7).
|
|
364
|
+
|
|
365
|
+
Let d = child.len, m = pos // d, r = pos % d.
|
|
366
|
+
"""
|
|
367
|
+
d = node.child.len
|
|
368
|
+
m, r = divmod(pos, d)
|
|
369
|
+
q = node.reps
|
|
370
|
+
|
|
371
|
+
if r == 0:
|
|
372
|
+
# Split falls on repetition boundary
|
|
373
|
+
left = _make_repeat(node.child, m, h)
|
|
374
|
+
right = _make_repeat(node.child, q - m, h)
|
|
375
|
+
return left, right
|
|
376
|
+
else:
|
|
377
|
+
# Split falls within a repetition
|
|
378
|
+
child_left, child_right = _split(node.child, r, h)
|
|
379
|
+
|
|
380
|
+
# Left = Repeat(child, m) ‖ child_left
|
|
381
|
+
left_rep = _make_repeat(node.child, m, h)
|
|
382
|
+
left = rope_concat(left_rep, child_left, h)
|
|
383
|
+
|
|
384
|
+
# Right = child_right ‖ Repeat(child, q - m - 1)
|
|
385
|
+
right_rep = _make_repeat(node.child, q - m - 1, h)
|
|
386
|
+
right = rope_concat(child_right, right_rep, h)
|
|
387
|
+
|
|
388
|
+
return left, right
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _make_repeat(
|
|
392
|
+
child: Leaf | Internal | RepeatNode, reps: int, h: PolynomialHash
|
|
393
|
+
) -> Node:
|
|
394
|
+
"""Create a Repeat with proper handling of reps 0 and 1."""
|
|
395
|
+
if reps == 0:
|
|
396
|
+
return None
|
|
397
|
+
if reps == 1:
|
|
398
|
+
return child
|
|
399
|
+
return RepeatNode(child, reps, h)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
# ---------------------------------------------------------------------------
|
|
403
|
+
# Theorem 8: Repeat
|
|
404
|
+
# ---------------------------------------------------------------------------
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def rope_repeat(
|
|
408
|
+
node: Leaf | Internal | RepeatNode,
|
|
409
|
+
q: int,
|
|
410
|
+
h: PolynomialHash,
|
|
411
|
+
) -> Node:
|
|
412
|
+
"""
|
|
413
|
+
Create a RepeatNode representing node^q (Theorem 8).
|
|
414
|
+
|
|
415
|
+
Time: O(k · log q) for the Φ computation. O(1) new nodes.
|
|
416
|
+
"""
|
|
417
|
+
if q == 0:
|
|
418
|
+
return None
|
|
419
|
+
if q == 1:
|
|
420
|
+
return node
|
|
421
|
+
return RepeatNode(node, q, h)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
# ---------------------------------------------------------------------------
|
|
425
|
+
# Theorem 9: SubstrHash (allocation-free)
|
|
426
|
+
# ---------------------------------------------------------------------------
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def rope_substr_hash(
|
|
430
|
+
node: Node,
|
|
431
|
+
start: int,
|
|
432
|
+
length: int,
|
|
433
|
+
h: PolynomialHash,
|
|
434
|
+
) -> int:
|
|
435
|
+
"""
|
|
436
|
+
Compute H(S[start..start+length-1]) without allocating nodes (Theorem 9).
|
|
437
|
+
|
|
438
|
+
Time: O(k · log w). Space: O(log w) stack.
|
|
439
|
+
"""
|
|
440
|
+
if node is None or length == 0:
|
|
441
|
+
return 0
|
|
442
|
+
return _hash_range(node, start, length, h)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _hash_range(
|
|
446
|
+
node: Leaf | Internal | RepeatNode,
|
|
447
|
+
start: int,
|
|
448
|
+
length: int,
|
|
449
|
+
h: PolynomialHash,
|
|
450
|
+
) -> int:
|
|
451
|
+
"""Recursive hash range query."""
|
|
452
|
+
p = h.prime
|
|
453
|
+
|
|
454
|
+
if isinstance(node, Leaf):
|
|
455
|
+
return h.hash(node.data[start:start + length])
|
|
456
|
+
|
|
457
|
+
if isinstance(node, Internal):
|
|
458
|
+
ll = node.left.len
|
|
459
|
+
if start + length <= ll:
|
|
460
|
+
# Entirely in left
|
|
461
|
+
return _hash_range(node.left, start, length, h)
|
|
462
|
+
if start >= ll:
|
|
463
|
+
# Entirely in right
|
|
464
|
+
return _hash_range(node.right, start - ll, length, h)
|
|
465
|
+
# Spanning
|
|
466
|
+
l_len = ll - start
|
|
467
|
+
r_len = length - l_len
|
|
468
|
+
h_l = _hash_range(node.left, start, l_len, h)
|
|
469
|
+
h_r = _hash_range(node.right, 0, r_len, h)
|
|
470
|
+
return h.hash_concat(h_l, r_len, h_r)
|
|
471
|
+
|
|
472
|
+
if isinstance(node, RepeatNode):
|
|
473
|
+
d = node.child.len
|
|
474
|
+
# Map into the repeating pattern
|
|
475
|
+
first_copy = start // d
|
|
476
|
+
start_in_copy = start % d
|
|
477
|
+
|
|
478
|
+
end = start + length - 1
|
|
479
|
+
last_copy = end // d
|
|
480
|
+
end_in_copy = end % d
|
|
481
|
+
|
|
482
|
+
if first_copy == last_copy:
|
|
483
|
+
# Entirely within one copy
|
|
484
|
+
return _hash_range(node.child, start_in_copy, length, h)
|
|
485
|
+
|
|
486
|
+
# Three parts: tail of first copy, full copies, head of last copy
|
|
487
|
+
# 1. Tail of first copy: child[start_in_copy .. d-1]
|
|
488
|
+
tail_len = d - start_in_copy
|
|
489
|
+
h_tail = _hash_range(node.child, start_in_copy, tail_len, h)
|
|
490
|
+
|
|
491
|
+
# 2. Full copies in the middle
|
|
492
|
+
full_copies = last_copy - first_copy - 1
|
|
493
|
+
if full_copies > 0:
|
|
494
|
+
x_d = h.power(d)
|
|
495
|
+
phi_val = phi(full_copies, x_d, p)
|
|
496
|
+
h_full = mersenne_mul(node.child.hash_val, phi_val, p)
|
|
497
|
+
else:
|
|
498
|
+
h_full = 0
|
|
499
|
+
full_len = full_copies * d
|
|
500
|
+
|
|
501
|
+
# 3. Head of last copy: child[0 .. end_in_copy]
|
|
502
|
+
head_len = end_in_copy + 1
|
|
503
|
+
h_head = _hash_range(node.child, 0, head_len, h)
|
|
504
|
+
|
|
505
|
+
# Combine: tail ‖ full ‖ head
|
|
506
|
+
# H(tail ‖ full ‖ head) = H(tail) · x^(full_len + head_len) + H(full) · x^(head_len) + H(head)
|
|
507
|
+
result = mersenne_mod(
|
|
508
|
+
h.power(full_len + head_len) * h_tail
|
|
509
|
+
+ h.power(head_len) * h_full
|
|
510
|
+
+ h_head,
|
|
511
|
+
p,
|
|
512
|
+
)
|
|
513
|
+
return result
|
|
514
|
+
|
|
515
|
+
raise TypeError(f"Unknown node type: {type(node)}")
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
# ---------------------------------------------------------------------------
|
|
519
|
+
# Reconstruction: rope → bytes
|
|
520
|
+
# ---------------------------------------------------------------------------
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def rope_to_bytes(node: Node) -> bytes:
|
|
524
|
+
"""Reconstruct the byte string from a rope (for testing)."""
|
|
525
|
+
if node is None:
|
|
526
|
+
return b""
|
|
527
|
+
parts: list[bytes] = []
|
|
528
|
+
_collect_bytes(node, parts)
|
|
529
|
+
return b"".join(parts)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _collect_bytes(node: Leaf | Internal | RepeatNode, parts: list[bytes]) -> None:
|
|
533
|
+
if isinstance(node, Leaf):
|
|
534
|
+
parts.append(node.data)
|
|
535
|
+
elif isinstance(node, Internal):
|
|
536
|
+
_collect_bytes(node.left, parts)
|
|
537
|
+
_collect_bytes(node.right, parts)
|
|
538
|
+
elif isinstance(node, RepeatNode):
|
|
539
|
+
for _ in range(node.reps):
|
|
540
|
+
_collect_bytes(node.child, parts)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
# ---------------------------------------------------------------------------
|
|
544
|
+
# Construction helper
|
|
545
|
+
# ---------------------------------------------------------------------------
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def rope_from_bytes(data: bytes, h: PolynomialHash) -> Node:
|
|
549
|
+
"""Create a rope from a byte string. Returns None for empty."""
|
|
550
|
+
if len(data) == 0:
|
|
551
|
+
return None
|
|
552
|
+
return Leaf(data, h)
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
# ---------------------------------------------------------------------------
|
|
556
|
+
# Validation (for testing)
|
|
557
|
+
# ---------------------------------------------------------------------------
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def validate_rope(node: Node, h: PolynomialHash | None = None) -> None:
|
|
561
|
+
"""
|
|
562
|
+
Validate all rope invariants (Definition 7). Raises AssertionError on violation.
|
|
563
|
+
"""
|
|
564
|
+
if node is None:
|
|
565
|
+
return
|
|
566
|
+
|
|
567
|
+
if isinstance(node, Leaf):
|
|
568
|
+
assert node.weight == 1, f"Leaf weight must be 1, got {node.weight}"
|
|
569
|
+
assert node.len == len(node.data), "Leaf len mismatch"
|
|
570
|
+
return
|
|
571
|
+
|
|
572
|
+
if isinstance(node, Internal):
|
|
573
|
+
# I2
|
|
574
|
+
assert node.len == node.left.len + node.right.len, "Internal len mismatch"
|
|
575
|
+
# I4
|
|
576
|
+
assert node.weight == node.left.weight + node.right.weight, "Internal weight mismatch"
|
|
577
|
+
# I8: balance
|
|
578
|
+
total = node.weight
|
|
579
|
+
wl = node.left.weight
|
|
580
|
+
if total > 2:
|
|
581
|
+
assert ALPHA_NUM * total <= ALPHA_DEN * wl, (
|
|
582
|
+
f"Left child too light: {wl}/{total}"
|
|
583
|
+
)
|
|
584
|
+
assert ALPHA_DEN * wl <= (ALPHA_DEN - ALPHA_NUM) * total, (
|
|
585
|
+
f"Left child too heavy: {wl}/{total}"
|
|
586
|
+
)
|
|
587
|
+
# Recurse
|
|
588
|
+
validate_rope(node.left, h)
|
|
589
|
+
validate_rope(node.right, h)
|
|
590
|
+
return
|
|
591
|
+
|
|
592
|
+
if isinstance(node, RepeatNode):
|
|
593
|
+
# I5
|
|
594
|
+
assert node.len == node.child.len * node.reps, "RepeatNode len mismatch"
|
|
595
|
+
# I7
|
|
596
|
+
assert node.weight == node.child.weight * node.reps, "RepeatNode weight mismatch"
|
|
597
|
+
assert node.reps >= 2, f"RepeatNode reps must be >= 2, got {node.reps}"
|
|
598
|
+
validate_rope(node.child, h)
|
|
599
|
+
return
|
|
600
|
+
|
|
601
|
+
raise TypeError(f"Unknown node type: {type(node)}")
|
hashrope/sliding.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sliding window hash rope for bounded-memory streaming hash computation.
|
|
3
|
+
|
|
4
|
+
Adapted from UHC's SlidingRopeState (Definition 10, Theorem 11).
|
|
5
|
+
Provides a generic sliding window over a hash rope without any
|
|
6
|
+
compression-format dependencies.
|
|
7
|
+
|
|
8
|
+
The window maintains a prefix hash and a bounded rope, evicting old
|
|
9
|
+
data as new data arrives. At any point, the full-sequence hash
|
|
10
|
+
H(T[0..pos-1]) can be computed in O(1) from the prefix hash and
|
|
11
|
+
the window rope hash.
|
|
12
|
+
|
|
13
|
+
Invariant I_slide (holds after every operation):
|
|
14
|
+
(1) h_prefix = H(T[0 .. l_prefix - 1])
|
|
15
|
+
(2) r_window represents T[l_prefix .. pos - 1]
|
|
16
|
+
(3) rope_len(r_window) = pos - l_prefix <= W
|
|
17
|
+
(4) H(T[0..pos-1]) = h_prefix * x^(pos - l_prefix) + rope_hash(r_window)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from hashrope.polynomial_hash import PolynomialHash, MERSENNE_61
|
|
23
|
+
from hashrope.rope import (
|
|
24
|
+
Leaf,
|
|
25
|
+
Node,
|
|
26
|
+
rope_concat,
|
|
27
|
+
rope_split,
|
|
28
|
+
rope_repeat,
|
|
29
|
+
rope_len,
|
|
30
|
+
rope_hash,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SlidingWindow:
|
|
35
|
+
"""
|
|
36
|
+
Bounded-memory sliding window over a hash rope.
|
|
37
|
+
|
|
38
|
+
Maintains a window of at most W = d_max + m_max bytes as a rope,
|
|
39
|
+
plus a prefix hash covering all evicted bytes. Supports two
|
|
40
|
+
operations:
|
|
41
|
+
|
|
42
|
+
- append_bytes(data): append literal bytes
|
|
43
|
+
- append_copy(offset, length): append a copy from within the window
|
|
44
|
+
|
|
45
|
+
At any point, current_hash() returns H(T[0..pos-1]) in O(1).
|
|
46
|
+
|
|
47
|
+
Parameters
|
|
48
|
+
----------
|
|
49
|
+
d_max : int
|
|
50
|
+
Maximum copy-back distance. The window retains at least d_max
|
|
51
|
+
bytes after eviction so that future copies can find their source.
|
|
52
|
+
m_max : int
|
|
53
|
+
Maximum copy length. Together with d_max, determines the
|
|
54
|
+
maximum window extent W = d_max + m_max.
|
|
55
|
+
prime : int
|
|
56
|
+
Mersenne prime for hash arithmetic. Default: 2^61 - 1.
|
|
57
|
+
base : int
|
|
58
|
+
Hash base. Default: 131.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
__slots__ = (
|
|
62
|
+
"h", "d_max", "m_max", "W",
|
|
63
|
+
"h_prefix", "l_prefix", "r_window", "pos",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
d_max: int = 32768,
|
|
69
|
+
m_max: int = 258,
|
|
70
|
+
prime: int = MERSENNE_61,
|
|
71
|
+
base: int = 131,
|
|
72
|
+
) -> None:
|
|
73
|
+
if d_max < 1:
|
|
74
|
+
raise ValueError(f"d_max must be >= 1, got {d_max}")
|
|
75
|
+
if m_max < 1:
|
|
76
|
+
raise ValueError(f"m_max must be >= 1, got {m_max}")
|
|
77
|
+
|
|
78
|
+
self.h = PolynomialHash(prime=prime, base=base)
|
|
79
|
+
self.d_max = d_max
|
|
80
|
+
self.m_max = m_max
|
|
81
|
+
self.W = d_max + m_max
|
|
82
|
+
|
|
83
|
+
# State (Definition 10)
|
|
84
|
+
self.h_prefix: int = 0 # H(T[0..l_prefix-1])
|
|
85
|
+
self.l_prefix: int = 0 # evicted byte count
|
|
86
|
+
self.r_window: Node = None # rope for T[l_prefix..pos-1]
|
|
87
|
+
self.pos: int = 0 # total bytes ingested
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def window_len(self) -> int:
|
|
91
|
+
"""Current window size in bytes."""
|
|
92
|
+
return rope_len(self.r_window)
|
|
93
|
+
|
|
94
|
+
def current_hash(self) -> int:
|
|
95
|
+
"""
|
|
96
|
+
Compute H(T[0..pos-1]) from sliding state via Theorem 1:
|
|
97
|
+
H(T) = H_prefix * x^(window_len) + H(window)
|
|
98
|
+
"""
|
|
99
|
+
wl = self.window_len
|
|
100
|
+
wh = rope_hash(self.r_window) if self.r_window else 0
|
|
101
|
+
return self.h.hash_concat(self.h_prefix, wl, wh)
|
|
102
|
+
|
|
103
|
+
def final_hash(self) -> int:
|
|
104
|
+
"""
|
|
105
|
+
FinalHash() -- Theorem 11.
|
|
106
|
+
|
|
107
|
+
H(T[0..N-1]) = H_prefix * x^(R_window.len) + R_window.hash
|
|
108
|
+
"""
|
|
109
|
+
return self.current_hash()
|
|
110
|
+
|
|
111
|
+
def append_bytes(self, data: bytes) -> None:
|
|
112
|
+
"""
|
|
113
|
+
Append literal bytes to the stream.
|
|
114
|
+
|
|
115
|
+
Parameters
|
|
116
|
+
----------
|
|
117
|
+
data : bytes
|
|
118
|
+
One or more bytes to append.
|
|
119
|
+
"""
|
|
120
|
+
if not data:
|
|
121
|
+
return
|
|
122
|
+
leaf = Leaf(data, self.h)
|
|
123
|
+
self.r_window = rope_concat(self.r_window, leaf, self.h)
|
|
124
|
+
self.pos += len(data)
|
|
125
|
+
self._evict()
|
|
126
|
+
|
|
127
|
+
def append_copy(self, offset: int, length: int) -> None:
|
|
128
|
+
"""
|
|
129
|
+
Append a copy-from-window reference.
|
|
130
|
+
|
|
131
|
+
Copies `length` bytes starting `offset` bytes back from the
|
|
132
|
+
current end of the window. Handles both non-overlapping
|
|
133
|
+
(offset >= length) and overlapping (offset < length) cases.
|
|
134
|
+
|
|
135
|
+
Parameters
|
|
136
|
+
----------
|
|
137
|
+
offset : int
|
|
138
|
+
Distance back from the current end of the window.
|
|
139
|
+
Must be <= pos and <= window_len.
|
|
140
|
+
length : int
|
|
141
|
+
Number of bytes to copy. For overlapping copies
|
|
142
|
+
(offset < length), the pattern of length `offset`
|
|
143
|
+
is repeated as needed.
|
|
144
|
+
|
|
145
|
+
Raises
|
|
146
|
+
------
|
|
147
|
+
ValueError
|
|
148
|
+
If offset exceeds decoded length or window size.
|
|
149
|
+
"""
|
|
150
|
+
win_len = self.window_len
|
|
151
|
+
|
|
152
|
+
if offset > self.pos:
|
|
153
|
+
raise ValueError(
|
|
154
|
+
f"Invalid copy: offset {offset} exceeds "
|
|
155
|
+
f"decoded length {self.pos}"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
if offset > win_len:
|
|
159
|
+
raise ValueError(
|
|
160
|
+
f"Copy offset {offset} exceeds window size {win_len}. "
|
|
161
|
+
f"Increase d_max (currently {self.d_max}) to at least {offset}."
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
start = win_len - offset
|
|
165
|
+
|
|
166
|
+
if offset >= length:
|
|
167
|
+
# Non-overlapping
|
|
168
|
+
_, tmp = rope_split(self.r_window, start, self.h)
|
|
169
|
+
source, _ = rope_split(tmp, length, self.h)
|
|
170
|
+
self.r_window = rope_concat(self.r_window, source, self.h)
|
|
171
|
+
else:
|
|
172
|
+
# Overlapping: extract pattern of length `offset`, repeat
|
|
173
|
+
_, tmp = rope_split(self.r_window, start, self.h)
|
|
174
|
+
pattern, _ = rope_split(tmp, offset, self.h)
|
|
175
|
+
|
|
176
|
+
q, r = divmod(length, offset)
|
|
177
|
+
rep: Node = rope_repeat(pattern, q, self.h) if q >= 1 else None
|
|
178
|
+
if r > 0:
|
|
179
|
+
partial, _ = rope_split(pattern, r, self.h)
|
|
180
|
+
rep = rope_concat(rep, partial, self.h)
|
|
181
|
+
self.r_window = rope_concat(self.r_window, rep, self.h)
|
|
182
|
+
|
|
183
|
+
self.pos += length
|
|
184
|
+
self._evict()
|
|
185
|
+
|
|
186
|
+
def _evict(self) -> None:
|
|
187
|
+
"""
|
|
188
|
+
Evict excess bytes from the window.
|
|
189
|
+
|
|
190
|
+
After eviction: rope_len(r_window) <= d_max, ensuring that
|
|
191
|
+
future copies of distance <= d_max find their source.
|
|
192
|
+
|
|
193
|
+
Correctness (Lemma 12): The prefix hash update follows from
|
|
194
|
+
Theorem 1 (concatenation composability).
|
|
195
|
+
"""
|
|
196
|
+
win_len = rope_len(self.r_window)
|
|
197
|
+
if win_len <= self.W:
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
# Evict down to d_max bytes remaining
|
|
201
|
+
excess = win_len - self.d_max
|
|
202
|
+
r_old, r_keep = rope_split(self.r_window, excess, self.h)
|
|
203
|
+
|
|
204
|
+
# Fold evicted bytes into h_prefix via Theorem 1
|
|
205
|
+
old_len = rope_len(r_old)
|
|
206
|
+
old_hash = rope_hash(r_old)
|
|
207
|
+
self.h_prefix = self.h.hash_concat(self.h_prefix, old_len, old_hash)
|
|
208
|
+
|
|
209
|
+
self.l_prefix += excess
|
|
210
|
+
self.r_window = r_keep
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hashrope
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Hash rope: a BB[2/7] balanced binary tree with polynomial hash metadata for O(log n) concat, split, substring hashing, and O(log q) repetition encoding.
|
|
5
|
+
Project-URL: Homepage, https://github.com/jemsbhai/hashrope
|
|
6
|
+
Project-URL: Repository, https://github.com/jemsbhai/hashrope
|
|
7
|
+
Project-URL: Issues, https://github.com/jemsbhai/hashrope/issues
|
|
8
|
+
Author-email: Muntaser Syed <muntaser@ieee.org>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: balanced-tree,content-addressing,data-structure,hash,mersenne-prime,polynomial-hash,rope
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# hashrope
|
|
31
|
+
|
|
32
|
+
A BB[2/7] weight-balanced binary tree augmented with polynomial hash metadata at every node.
|
|
33
|
+
|
|
34
|
+
[](https://pypi.org/project/hashrope/)
|
|
35
|
+
[](https://pypi.org/project/hashrope/)
|
|
36
|
+
[](https://opensource.org/licenses/MIT)
|
|
37
|
+
[](https://github.com/jemsbhai/hashrope)
|
|
38
|
+
|
|
39
|
+
**Zero dependencies. Pure Python. Python 3.10+.**
|
|
40
|
+
|
|
41
|
+
## What it does
|
|
42
|
+
|
|
43
|
+
hashrope is a rope data structure where every node carries a polynomial hash. This enables:
|
|
44
|
+
|
|
45
|
+
- **O(k log w) concat and split** with strict BB[2/7] rebalancing via recursive join
|
|
46
|
+
- **O(k log q) repetition encoding** via RepeatNode -- represents "repeat this subtree q times" without materializing the data
|
|
47
|
+
- **O(k log w) substring hashing** without allocating new nodes
|
|
48
|
+
- **O(1) whole-sequence hash** at any point during streaming ingestion
|
|
49
|
+
- **Bounded-memory sliding window** that evicts old data while maintaining a running hash of everything seen
|
|
50
|
+
|
|
51
|
+
All arithmetic uses Mersenne prime modular reduction (bit-shift, no division).
|
|
52
|
+
|
|
53
|
+
## Install
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
pip install hashrope
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Quick start
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from hashrope import PolynomialHash, Leaf, rope_concat, rope_split, rope_substr_hash, rope_hash
|
|
63
|
+
|
|
64
|
+
h = PolynomialHash()
|
|
65
|
+
|
|
66
|
+
# Build a rope
|
|
67
|
+
a = Leaf(b"hello ", h)
|
|
68
|
+
b = Leaf(b"world", h)
|
|
69
|
+
ab = rope_concat(a, b, h)
|
|
70
|
+
|
|
71
|
+
# Whole-string hash matches direct computation
|
|
72
|
+
assert rope_hash(ab) == h.hash(b"hello world")
|
|
73
|
+
|
|
74
|
+
# Split at any byte position
|
|
75
|
+
left, right = rope_split(ab, 6, h)
|
|
76
|
+
|
|
77
|
+
# Substring hash without allocation
|
|
78
|
+
sub_h = rope_substr_hash(ab, 0, 5, h)
|
|
79
|
+
assert sub_h == h.hash(b"hello")
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### RepeatNode
|
|
83
|
+
|
|
84
|
+
Encode repetitions in O(log q) time and O(1) space:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from hashrope import PolynomialHash, Leaf, rope_repeat, rope_hash
|
|
88
|
+
|
|
89
|
+
h = PolynomialHash()
|
|
90
|
+
pattern = Leaf(b"ab", h)
|
|
91
|
+
repeated = rope_repeat(pattern, 1000, h) # "ab" x 1000
|
|
92
|
+
|
|
93
|
+
# Hash computed in O(log 1000), not O(2000)
|
|
94
|
+
assert rope_hash(repeated) == h.hash(b"ab" * 1000)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### SlidingWindow
|
|
98
|
+
|
|
99
|
+
Bounded-memory streaming hash with copy-from-window support:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from hashrope import PolynomialHash, SlidingWindow
|
|
103
|
+
|
|
104
|
+
h = PolynomialHash()
|
|
105
|
+
sw = SlidingWindow(d_max=1024, m_max=256)
|
|
106
|
+
|
|
107
|
+
# Stream data in
|
|
108
|
+
sw.append_bytes(b"hello ")
|
|
109
|
+
sw.append_bytes(b"world")
|
|
110
|
+
|
|
111
|
+
# Hash of everything seen so far
|
|
112
|
+
assert sw.current_hash() == h.hash(b"hello world")
|
|
113
|
+
|
|
114
|
+
# Copy-from-window (like LZ77 back-references)
|
|
115
|
+
sw.append_copy(offset=5, length=5) # copies "world" again
|
|
116
|
+
assert sw.final_hash() == h.hash(b"hello worldworld")
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## API reference
|
|
120
|
+
|
|
121
|
+
### Polynomial hash
|
|
122
|
+
|
|
123
|
+
| Function / Class | Description |
|
|
124
|
+
|---|---|
|
|
125
|
+
| `PolynomialHash(prime, base)` | Hash function over Z/pZ. Default: p = 2^61 - 1, base = 131 |
|
|
126
|
+
| `PolynomialHash.hash(data)` | H(data) with +1 byte offset to prevent zero collisions |
|
|
127
|
+
| `PolynomialHash.hash_concat(h_a, len_b, h_b)` | H(A \|\| B) from H(A), \|B\|, H(B) in O(1) |
|
|
128
|
+
| `PolynomialHash.hash_repeat(h_s, d, q)` | H(S^q) from H(S) and \|S\| in O(log q) |
|
|
129
|
+
| `PolynomialHash.hash_overlap(h_p, d, l, h_prefix)` | H(P^q \|\| P[0..r-1]) for overlapping references |
|
|
130
|
+
| `phi(q, alpha, p)` | Geometric accumulator: sum of alpha^i for i in [0, q) in O(log q) |
|
|
131
|
+
| `mersenne_mod(a, p)` | Bit-shift reduction mod Mersenne prime |
|
|
132
|
+
| `mersenne_mul(a, b, p)` | Modular multiply via mersenne_mod |
|
|
133
|
+
| `MERSENNE_61` | 2^61 - 1 (default prime) |
|
|
134
|
+
| `MERSENNE_127` | 2^127 - 1 (high-security prime) |
|
|
135
|
+
|
|
136
|
+
### Rope nodes
|
|
137
|
+
|
|
138
|
+
| Class | Description |
|
|
139
|
+
|---|---|
|
|
140
|
+
| `Leaf(data, h)` | Immutable leaf storing a byte sequence. Weight = 1 |
|
|
141
|
+
| `Internal(left, right, h)` | Internal node. Hash = H(left) \* x^len(right) + H(right) |
|
|
142
|
+
| `RepeatNode(child, reps, h)` | Represents child^reps. Hash via geometric accumulator |
|
|
143
|
+
|
|
144
|
+
### Rope operations
|
|
145
|
+
|
|
146
|
+
| Function | Time | Description |
|
|
147
|
+
|---|---|---|
|
|
148
|
+
| `rope_concat(left, right, h)` | O(k \|h_L - h_R\|) | Concatenate with recursive join and BB[2/7] rebalancing |
|
|
149
|
+
| `rope_split(node, pos, h)` | O(k log w) | Split at byte position |
|
|
150
|
+
| `rope_repeat(node, q, h)` | O(k log q) | Create RepeatNode |
|
|
151
|
+
| `rope_substr_hash(node, start, length, h)` | O(k log w) | Substring hash without allocation |
|
|
152
|
+
| `rope_len(node)` | O(1) | Total byte length |
|
|
153
|
+
| `rope_hash(node)` | O(1) | Hash of represented string |
|
|
154
|
+
| `rope_from_bytes(data, h)` | O(n) | Create Leaf from bytes |
|
|
155
|
+
| `rope_to_bytes(node)` | O(n) | Reconstruct bytes (for testing) |
|
|
156
|
+
| `validate_rope(node)` | O(w) | Assert all 9 invariants hold |
|
|
157
|
+
|
|
158
|
+
### SlidingWindow
|
|
159
|
+
|
|
160
|
+
| Method | Description |
|
|
161
|
+
|---|---|
|
|
162
|
+
| `SlidingWindow(d_max, m_max, prime, base)` | Create window. W = d_max + m_max |
|
|
163
|
+
| `.append_bytes(data)` | Append literal bytes |
|
|
164
|
+
| `.append_copy(offset, length)` | Copy from window (handles overlapping) |
|
|
165
|
+
| `.current_hash()` | H(everything seen) in O(1) |
|
|
166
|
+
| `.final_hash()` | Same as current_hash (alias) |
|
|
167
|
+
| `.window_len` | Current window size in bytes |
|
|
168
|
+
| `.pos` | Total bytes ingested |
|
|
169
|
+
|
|
170
|
+
## Mathematical foundation
|
|
171
|
+
|
|
172
|
+
The data structure and its correctness proofs come from the compressed-domain hashing framework (24 theorems, 14 lemmas, 5 corollaries). Key results used by hashrope:
|
|
173
|
+
|
|
174
|
+
| Result | Statement |
|
|
175
|
+
|---|---|
|
|
176
|
+
| Theorem 1 (Concatenation) | H(A \|\| B) = H(A) * x^{\|B\|} + H(B) |
|
|
177
|
+
| Theorem 2 (Repetition) | H(S^q) = H(S) * Phi(q, x^d) |
|
|
178
|
+
| Theorem 3 (Phi computation) | Phi(q, alpha) computable in O(log q) without modular inverse |
|
|
179
|
+
| Theorem 6 (Join) | Recursive join with BB[2/7] rebalancing |
|
|
180
|
+
| Theorem 7 (Split) | Split in O(k log w) with RepeatNode splittability |
|
|
181
|
+
| Theorem 8 (Repeat) | RepeatNode in O(k log q), O(1) space |
|
|
182
|
+
| Theorem 9 (SubstrHash) | Allocation-free substring hashing in O(k log w) |
|
|
183
|
+
| Theorem 11 (Sliding window) | Bounded-memory streaming with eviction |
|
|
184
|
+
|
|
185
|
+
## Provenance
|
|
186
|
+
|
|
187
|
+
Extracted from [UHC](https://github.com/jemsbhai/uhc) (Unified Hash-Compression Engine). The rope and polynomial hash modules are verbatim copies with import paths updated. The sliding window is adapted from UHC's SlidingRopeState with the LZ77 dependency removed.
|
|
188
|
+
|
|
189
|
+
## License
|
|
190
|
+
|
|
191
|
+
MIT
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
hashrope/__init__.py,sha256=Sx2C3jXNV5_mFIRc9GnuazzX4ZgBrpK6lwYLCH9E_AY,1383
|
|
2
|
+
hashrope/polynomial_hash.py,sha256=W45PrlkK4OJi_a7QHdTOMFZqYvTNJrLqxjXI3RIdjDc,7520
|
|
3
|
+
hashrope/rope.py,sha256=USsOegxBOqABiutYL6xSt6pDyrSCU5goWf6g8P-_Enk,19362
|
|
4
|
+
hashrope/sliding.py,sha256=VoKpmxvyl7UqzCMKgQaMGi8Plt0l6EubMqHu4TKO_58,6926
|
|
5
|
+
hashrope-0.1.0.dist-info/METADATA,sha256=gLMTmqb1LJcYplyOptSSQLfp1qXy2TpqaPCJd73EdFU,7400
|
|
6
|
+
hashrope-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
7
|
+
hashrope-0.1.0.dist-info/licenses/LICENSE,sha256=G8xR12Tb3ogZTszzaEMdOQYzc5ZpSvIHbiTm0bPIYpA,1091
|
|
8
|
+
hashrope-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Muntaser Syed
|
|
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.
|