react-native-ble-mesh 1.1.1 → 2.0.0
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.
- package/README.md +288 -172
- package/docs/IOS-BACKGROUND-BLE.md +231 -0
- package/docs/OPTIMIZATION.md +70 -0
- package/docs/SPEC-v2.1.md +308 -0
- package/package.json +1 -1
- package/src/MeshNetwork.js +659 -465
- package/src/constants/index.js +1 -0
- package/src/crypto/AutoCrypto.js +79 -0
- package/src/crypto/CryptoProvider.js +99 -0
- package/src/crypto/index.js +15 -63
- package/src/crypto/providers/ExpoCryptoProvider.js +125 -0
- package/src/crypto/providers/QuickCryptoProvider.js +134 -0
- package/src/crypto/providers/TweetNaClProvider.js +124 -0
- package/src/crypto/providers/index.js +11 -0
- package/src/errors/MeshError.js +2 -1
- package/src/expo/withBLEMesh.js +102 -0
- package/src/hooks/useMesh.js +30 -9
- package/src/hooks/useMessages.js +2 -0
- package/src/index.js +23 -8
- package/src/mesh/dedup/DedupManager.js +36 -10
- package/src/mesh/fragment/Assembler.js +5 -0
- package/src/mesh/index.js +1 -1
- package/src/mesh/monitor/ConnectionQuality.js +408 -0
- package/src/mesh/monitor/NetworkMonitor.js +327 -316
- package/src/mesh/monitor/index.js +7 -3
- package/src/mesh/peer/PeerManager.js +6 -1
- package/src/mesh/router/MessageRouter.js +26 -15
- package/src/mesh/router/RouteTable.js +7 -1
- package/src/mesh/store/StoreAndForwardManager.js +295 -297
- package/src/mesh/store/index.js +1 -1
- package/src/service/BatteryOptimizer.js +282 -278
- package/src/service/EmergencyManager.js +224 -214
- package/src/service/HandshakeManager.js +167 -13
- package/src/service/MeshService.js +72 -6
- package/src/service/SessionManager.js +77 -2
- package/src/service/audio/AudioManager.js +8 -2
- package/src/service/file/FileAssembler.js +106 -0
- package/src/service/file/FileChunker.js +79 -0
- package/src/service/file/FileManager.js +307 -0
- package/src/service/file/FileMessage.js +122 -0
- package/src/service/file/index.js +15 -0
- package/src/service/text/broadcast/BroadcastManager.js +16 -0
- package/src/transport/BLETransport.js +131 -9
- package/src/transport/MockTransport.js +1 -1
- package/src/transport/MultiTransport.js +305 -0
- package/src/transport/WiFiDirectTransport.js +295 -0
- package/src/transport/adapters/NodeBLEAdapter.js +34 -0
- package/src/transport/adapters/RNBLEAdapter.js +56 -1
- package/src/transport/index.js +6 -0
- package/src/utils/compression.js +291 -291
- package/src/crypto/aead.js +0 -189
- package/src/crypto/chacha20.js +0 -181
- package/src/crypto/hkdf.js +0 -187
- package/src/crypto/hmac.js +0 -143
- package/src/crypto/keys/KeyManager.js +0 -271
- package/src/crypto/keys/KeyPair.js +0 -216
- package/src/crypto/keys/SecureStorage.js +0 -219
- package/src/crypto/keys/index.js +0 -32
- package/src/crypto/noise/handshake.js +0 -410
- package/src/crypto/noise/index.js +0 -27
- package/src/crypto/noise/session.js +0 -253
- package/src/crypto/noise/state.js +0 -268
- package/src/crypto/poly1305.js +0 -113
- package/src/crypto/sha256.js +0 -240
- package/src/crypto/x25519.js +0 -154
package/src/utils/compression.js
CHANGED
|
@@ -18,14 +18,14 @@ const { ValidationError, MeshError } = require('../errors');
|
|
|
18
18
|
* @constant {Object}
|
|
19
19
|
*/
|
|
20
20
|
const DEFAULT_CONFIG = Object.freeze({
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
/** Minimum payload size to compress (bytes) */
|
|
22
|
+
threshold: 100,
|
|
23
|
+
/** Hash table size for compression (power of 2) */
|
|
24
|
+
hashTableSize: 4096,
|
|
25
|
+
/** Minimum match length */
|
|
26
|
+
minMatch: 4,
|
|
27
|
+
/** Maximum match search distance */
|
|
28
|
+
maxSearchDistance: 65535
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
/**
|
|
@@ -43,275 +43,275 @@ const DEFAULT_CONFIG = Object.freeze({
|
|
|
43
43
|
* const original = compressor.decompress(data, compressed);
|
|
44
44
|
*/
|
|
45
45
|
class MessageCompressor {
|
|
46
|
-
|
|
46
|
+
/**
|
|
47
47
|
* Creates a new MessageCompressor instance.
|
|
48
48
|
* @param {Object} [options={}] - Compression options
|
|
49
49
|
* @param {number} [options.threshold=100] - Min size to compress
|
|
50
50
|
*/
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
51
|
+
constructor(options = {}) {
|
|
52
|
+
this._config = { ...DEFAULT_CONFIG, ...options };
|
|
53
|
+
this._stats = {
|
|
54
|
+
compressionAttempts: 0,
|
|
55
|
+
successfulCompressions: 0,
|
|
56
|
+
decompressions: 0,
|
|
57
|
+
bytesIn: 0,
|
|
58
|
+
bytesOut: 0
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
63
|
* Compresses a payload if it exceeds the threshold.
|
|
64
64
|
* @param {Uint8Array} payload - Payload to compress
|
|
65
65
|
* @returns {{ data: Uint8Array, compressed: boolean }} Result
|
|
66
66
|
*/
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
// Don't compress if below threshold
|
|
73
|
-
if (payload.length < this._config.threshold) {
|
|
74
|
-
return { data: payload, compressed: false };
|
|
75
|
-
}
|
|
67
|
+
compress(payload) {
|
|
68
|
+
if (!(payload instanceof Uint8Array)) {
|
|
69
|
+
throw ValidationError.invalidType('payload', payload, 'Uint8Array');
|
|
70
|
+
}
|
|
76
71
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const compressed = this._lz4Compress(payload);
|
|
82
|
-
|
|
83
|
-
// Only use compressed if it's actually smaller
|
|
84
|
-
if (compressed.length < payload.length) {
|
|
85
|
-
this._stats.successfulCompressions++;
|
|
86
|
-
this._stats.bytesOut += compressed.length;
|
|
87
|
-
return { data: compressed, compressed: true };
|
|
88
|
-
}
|
|
89
|
-
} catch (error) {
|
|
90
|
-
// Log compression error at debug level for troubleshooting
|
|
91
|
-
if (typeof console !== 'undefined' && console.debug) {
|
|
92
|
-
console.debug('Compression failed, using uncompressed:', error.message);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
72
|
+
// Don't compress if below threshold
|
|
73
|
+
if (payload.length < this._config.threshold) {
|
|
74
|
+
return { data: payload, compressed: false };
|
|
75
|
+
}
|
|
95
76
|
|
|
96
|
-
|
|
97
|
-
|
|
77
|
+
this._stats.compressionAttempts++;
|
|
78
|
+
this._stats.bytesIn += payload.length;
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const compressed = this._lz4Compress(payload);
|
|
82
|
+
|
|
83
|
+
// Only use compressed if it's actually smaller
|
|
84
|
+
if (compressed.length < payload.length) {
|
|
85
|
+
this._stats.successfulCompressions++;
|
|
86
|
+
this._stats.bytesOut += compressed.length;
|
|
87
|
+
return { data: compressed, compressed: true };
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
// Log compression error at debug level for troubleshooting
|
|
91
|
+
if (typeof console !== 'undefined' && console.debug) {
|
|
92
|
+
console.debug('Compression failed, using uncompressed:', error.message);
|
|
93
|
+
}
|
|
98
94
|
}
|
|
99
95
|
|
|
100
|
-
|
|
96
|
+
this._stats.bytesOut += payload.length;
|
|
97
|
+
return { data: payload, compressed: false };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
101
|
* Decompresses a payload.
|
|
102
102
|
* @param {Uint8Array} payload - Payload to decompress
|
|
103
103
|
* @param {boolean} wasCompressed - Whether payload was compressed
|
|
104
104
|
* @returns {Uint8Array} Decompressed data
|
|
105
105
|
*/
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (!(payload instanceof Uint8Array)) {
|
|
112
|
-
throw ValidationError.invalidType('payload', payload, 'Uint8Array');
|
|
113
|
-
}
|
|
106
|
+
decompress(payload, wasCompressed) {
|
|
107
|
+
if (!wasCompressed) {
|
|
108
|
+
return payload;
|
|
109
|
+
}
|
|
114
110
|
|
|
115
|
-
|
|
116
|
-
|
|
111
|
+
if (!(payload instanceof Uint8Array)) {
|
|
112
|
+
throw ValidationError.invalidType('payload', payload, 'Uint8Array');
|
|
117
113
|
}
|
|
118
114
|
|
|
119
|
-
|
|
115
|
+
this._stats.decompressions++;
|
|
116
|
+
return this._lz4Decompress(payload);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
120
|
* Gets compression ratio for a payload.
|
|
121
121
|
* @param {Uint8Array} original - Original payload
|
|
122
122
|
* @param {Uint8Array} compressed - Compressed payload
|
|
123
123
|
* @returns {number} Compression ratio (0-100%)
|
|
124
124
|
*/
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
125
|
+
getCompressionRatio(original, compressed) {
|
|
126
|
+
if (original.length === 0) { return 0; }
|
|
127
|
+
return (1 - compressed.length / original.length) * 100;
|
|
128
|
+
}
|
|
129
129
|
|
|
130
|
-
|
|
130
|
+
/**
|
|
131
131
|
* Gets compression statistics.
|
|
132
132
|
* @returns {Object} Statistics
|
|
133
133
|
*/
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
134
|
+
getStats() {
|
|
135
|
+
const ratio = this._stats.bytesIn > 0
|
|
136
|
+
? (1 - this._stats.bytesOut / this._stats.bytesIn) * 100
|
|
137
|
+
: 0;
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
...this._stats,
|
|
141
|
+
averageCompressionRatio: ratio,
|
|
142
|
+
compressionRate: this._stats.compressionAttempts > 0
|
|
143
|
+
? this._stats.successfulCompressions / this._stats.compressionAttempts
|
|
144
|
+
: 0
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
149
|
* Resets statistics.
|
|
150
150
|
*/
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
151
|
+
resetStats() {
|
|
152
|
+
this._stats = {
|
|
153
|
+
compressionAttempts: 0,
|
|
154
|
+
successfulCompressions: 0,
|
|
155
|
+
decompressions: 0,
|
|
156
|
+
bytesIn: 0,
|
|
157
|
+
bytesOut: 0
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
162
|
* LZ4 compression implementation.
|
|
163
163
|
* @param {Uint8Array} input - Input data
|
|
164
164
|
* @returns {Uint8Array} Compressed data
|
|
165
165
|
* @private
|
|
166
166
|
*/
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
167
|
+
_lz4Compress(input) {
|
|
168
|
+
const inputLen = input.length;
|
|
169
|
+
|
|
170
|
+
// Worst case: no compression + header + margin
|
|
171
|
+
const output = new Uint8Array(inputLen + Math.ceil(inputLen / 255) + 16);
|
|
172
|
+
let outputPos = 0;
|
|
173
|
+
|
|
174
|
+
// Write original size (4 bytes, little-endian)
|
|
175
|
+
output[outputPos++] = inputLen & 0xff;
|
|
176
|
+
output[outputPos++] = (inputLen >> 8) & 0xff;
|
|
177
|
+
output[outputPos++] = (inputLen >> 16) & 0xff;
|
|
178
|
+
output[outputPos++] = (inputLen >> 24) & 0xff;
|
|
179
|
+
|
|
180
|
+
// Hash table for finding matches
|
|
181
|
+
// Using Knuth's multiplicative hash constant (2654435761) for good distribution
|
|
182
|
+
const hashTable = new Int32Array(this._config.hashTableSize);
|
|
183
|
+
hashTable.fill(-1);
|
|
184
|
+
|
|
185
|
+
let anchor = 0;
|
|
186
|
+
let inputPos = 0;
|
|
187
|
+
|
|
188
|
+
while (inputPos < inputLen - 4) {
|
|
189
|
+
// Calculate hash
|
|
190
|
+
const hash = this._hash4(input, inputPos);
|
|
191
|
+
const matchPos = hashTable[hash];
|
|
192
|
+
hashTable[hash] = inputPos;
|
|
193
|
+
|
|
194
|
+
// Check if we have a match
|
|
195
|
+
if (matchPos >= 0 && inputPos - matchPos <= this._config.maxSearchDistance) {
|
|
196
|
+
// Verify match at least starts correctly
|
|
197
|
+
if (input[matchPos] === input[inputPos] &&
|
|
198
198
|
input[matchPos + 1] === input[inputPos + 1] &&
|
|
199
199
|
input[matchPos + 2] === input[inputPos + 2] &&
|
|
200
200
|
input[matchPos + 3] === input[inputPos + 3]) {
|
|
201
201
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
202
|
+
// Extend the match
|
|
203
|
+
let matchLen = 4;
|
|
204
|
+
while (inputPos + matchLen < inputLen &&
|
|
205
205
|
input[matchPos + matchLen] === input[inputPos + matchLen]) {
|
|
206
|
-
|
|
207
|
-
|
|
206
|
+
matchLen++;
|
|
207
|
+
}
|
|
208
208
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
209
|
+
if (matchLen >= this._config.minMatch) {
|
|
210
|
+
// Write literals before match
|
|
211
|
+
const literalLen = inputPos - anchor;
|
|
212
|
+
const offset = inputPos - matchPos;
|
|
213
213
|
|
|
214
|
-
|
|
214
|
+
outputPos = this._writeSequence(output, outputPos, input, anchor, literalLen, matchLen, offset);
|
|
215
215
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
inputPos++;
|
|
216
|
+
inputPos += matchLen;
|
|
217
|
+
anchor = inputPos;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
224
220
|
}
|
|
221
|
+
}
|
|
225
222
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (literalLen > 0) {
|
|
229
|
-
outputPos = this._writeFinalLiterals(output, outputPos, input, anchor, literalLen);
|
|
230
|
-
}
|
|
223
|
+
inputPos++;
|
|
224
|
+
}
|
|
231
225
|
|
|
232
|
-
|
|
226
|
+
// Write remaining literals (final block with no match)
|
|
227
|
+
const literalLen = inputLen - anchor;
|
|
228
|
+
if (literalLen > 0) {
|
|
229
|
+
outputPos = this._writeFinalLiterals(output, outputPos, input, anchor, literalLen);
|
|
233
230
|
}
|
|
234
231
|
|
|
235
|
-
|
|
232
|
+
return output.subarray(0, outputPos);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
236
|
* LZ4 decompression implementation.
|
|
237
237
|
* @param {Uint8Array} input - Compressed data
|
|
238
238
|
* @returns {Uint8Array} Decompressed data
|
|
239
239
|
* @private
|
|
240
240
|
*/
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const output = new Uint8Array(originalSize);
|
|
254
|
-
let inputPos = 4;
|
|
255
|
-
let outputPos = 0;
|
|
256
|
-
|
|
257
|
-
while (inputPos < input.length && outputPos < originalSize) {
|
|
258
|
-
// Read token
|
|
259
|
-
const token = input[inputPos++];
|
|
260
|
-
let literalLen = token >> 4;
|
|
261
|
-
let matchLen = token & 0x0f;
|
|
262
|
-
|
|
263
|
-
// Read extended literal length
|
|
264
|
-
if (literalLen === 15) {
|
|
265
|
-
let byte;
|
|
266
|
-
do {
|
|
267
|
-
byte = input[inputPos++];
|
|
268
|
-
literalLen += byte;
|
|
269
|
-
} while (byte === 255);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// Copy literals
|
|
273
|
-
for (let i = 0; i < literalLen && outputPos < originalSize; i++) {
|
|
274
|
-
output[outputPos++] = input[inputPos++];
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Check if we're at the end (last sequence has no match)
|
|
278
|
-
if (outputPos >= originalSize || inputPos >= input.length) {
|
|
279
|
-
break;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Read offset (2 bytes, little-endian)
|
|
283
|
-
const offset = input[inputPos] | (input[inputPos + 1] << 8);
|
|
284
|
-
inputPos += 2;
|
|
285
|
-
|
|
286
|
-
if (offset === 0) {
|
|
287
|
-
throw new MeshError(
|
|
288
|
-
'Invalid offset in compressed data: zero offset not allowed',
|
|
289
|
-
'E900',
|
|
290
|
-
{ inputPos, outputPos }
|
|
291
|
-
);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Read extended match length
|
|
295
|
-
matchLen += 4; // Minimum match is 4
|
|
296
|
-
if ((token & 0x0f) === 15) {
|
|
297
|
-
let byte;
|
|
298
|
-
do {
|
|
299
|
-
byte = input[inputPos++];
|
|
300
|
-
matchLen += byte;
|
|
301
|
-
} while (byte === 255);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Copy match
|
|
305
|
-
const matchStart = outputPos - offset;
|
|
306
|
-
for (let i = 0; i < matchLen && outputPos < originalSize; i++) {
|
|
307
|
-
output[outputPos++] = output[matchStart + i];
|
|
308
|
-
}
|
|
309
|
-
}
|
|
241
|
+
_lz4Decompress(input) {
|
|
242
|
+
// Read original size
|
|
243
|
+
const originalSize = input[0] | (input[1] << 8) | (input[2] << 16) | (input[3] << 24);
|
|
244
|
+
|
|
245
|
+
if (originalSize <= 0 || originalSize > 100 * 1024 * 1024) {
|
|
246
|
+
throw new MeshError(
|
|
247
|
+
'Invalid compressed data: size header out of range',
|
|
248
|
+
'E900',
|
|
249
|
+
{ originalSize, maxAllowed: 100 * 1024 * 1024 }
|
|
250
|
+
);
|
|
251
|
+
}
|
|
310
252
|
|
|
311
|
-
|
|
253
|
+
const output = new Uint8Array(originalSize);
|
|
254
|
+
let inputPos = 4;
|
|
255
|
+
let outputPos = 0;
|
|
256
|
+
|
|
257
|
+
while (inputPos < input.length && outputPos < originalSize) {
|
|
258
|
+
// Read token
|
|
259
|
+
const token = input[inputPos++];
|
|
260
|
+
let literalLen = token >> 4;
|
|
261
|
+
let matchLen = token & 0x0f;
|
|
262
|
+
|
|
263
|
+
// Read extended literal length
|
|
264
|
+
if (literalLen === 15) {
|
|
265
|
+
let byte;
|
|
266
|
+
do {
|
|
267
|
+
byte = input[inputPos++];
|
|
268
|
+
literalLen += byte;
|
|
269
|
+
} while (byte === 255);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Copy literals
|
|
273
|
+
for (let i = 0; i < literalLen && outputPos < originalSize; i++) {
|
|
274
|
+
output[outputPos++] = input[inputPos++];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Check if we're at the end (last sequence has no match)
|
|
278
|
+
if (outputPos >= originalSize || inputPos >= input.length) {
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Read offset (2 bytes, little-endian)
|
|
283
|
+
const offset = input[inputPos] | (input[inputPos + 1] << 8);
|
|
284
|
+
inputPos += 2;
|
|
285
|
+
|
|
286
|
+
if (offset === 0) {
|
|
287
|
+
throw new MeshError(
|
|
288
|
+
'Invalid offset in compressed data: zero offset not allowed',
|
|
289
|
+
'E900',
|
|
290
|
+
{ inputPos, outputPos }
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Read extended match length
|
|
295
|
+
matchLen += 4; // Minimum match is 4
|
|
296
|
+
if ((token & 0x0f) === 15) {
|
|
297
|
+
let byte;
|
|
298
|
+
do {
|
|
299
|
+
byte = input[inputPos++];
|
|
300
|
+
matchLen += byte;
|
|
301
|
+
} while (byte === 255);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Copy match
|
|
305
|
+
const matchStart = outputPos - offset;
|
|
306
|
+
for (let i = 0; i < matchLen && outputPos < originalSize; i++) {
|
|
307
|
+
output[outputPos++] = output[matchStart + i];
|
|
308
|
+
}
|
|
312
309
|
}
|
|
313
310
|
|
|
314
|
-
|
|
311
|
+
return output.subarray(0, outputPos);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
315
|
* Calculates hash for 4 bytes using Knuth's multiplicative hash.
|
|
316
316
|
*
|
|
317
317
|
* Uses the constant 2654435761 (0x9E3779B1), which is derived from the
|
|
@@ -327,15 +327,15 @@ class MessageCompressor {
|
|
|
327
327
|
* @returns {number} Hash value in range [0, hashTableSize)
|
|
328
328
|
* @private
|
|
329
329
|
*/
|
|
330
|
-
|
|
331
|
-
|
|
330
|
+
_hash4(data, pos) {
|
|
331
|
+
const val = data[pos] | (data[pos + 1] << 8) |
|
|
332
332
|
(data[pos + 2] << 16) | (data[pos + 3] << 24);
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
333
|
+
// Knuth's multiplicative hash: multiply by golden ratio constant
|
|
334
|
+
// The >>> 0 ensures unsigned 32-bit arithmetic
|
|
335
|
+
return ((val * 2654435761) >>> 0) % this._config.hashTableSize;
|
|
336
|
+
}
|
|
337
337
|
|
|
338
|
-
|
|
338
|
+
/**
|
|
339
339
|
* Writes a complete LZ4 sequence (literals + match).
|
|
340
340
|
* @param {Uint8Array} output - Output buffer
|
|
341
341
|
* @param {number} pos - Current position
|
|
@@ -347,48 +347,48 @@ class MessageCompressor {
|
|
|
347
347
|
* @returns {number} New position
|
|
348
348
|
* @private
|
|
349
349
|
*/
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
// Copy literals
|
|
370
|
-
for (let i = 0; i < literalLen; i++) {
|
|
371
|
-
output[pos++] = input[literalStart + i];
|
|
372
|
-
}
|
|
350
|
+
_writeSequence(output, pos, input, literalStart, literalLen, matchLen, offset) {
|
|
351
|
+
// Adjust match length (minimum is 4, stored as matchLen - 4)
|
|
352
|
+
const adjustedMatchLen = matchLen - 4;
|
|
353
|
+
|
|
354
|
+
// Build token
|
|
355
|
+
const literalToken = Math.min(literalLen, 15);
|
|
356
|
+
const matchToken = Math.min(adjustedMatchLen, 15);
|
|
357
|
+
output[pos++] = (literalToken << 4) | matchToken;
|
|
358
|
+
|
|
359
|
+
// Write extended literal length
|
|
360
|
+
if (literalLen >= 15) {
|
|
361
|
+
let remaining = literalLen - 15;
|
|
362
|
+
while (remaining >= 255) {
|
|
363
|
+
output[pos++] = 255;
|
|
364
|
+
remaining -= 255;
|
|
365
|
+
}
|
|
366
|
+
output[pos++] = remaining;
|
|
367
|
+
}
|
|
373
368
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
// Write extended match length
|
|
379
|
-
if (adjustedMatchLen >= 15) {
|
|
380
|
-
let remaining = adjustedMatchLen - 15;
|
|
381
|
-
while (remaining >= 255) {
|
|
382
|
-
output[pos++] = 255;
|
|
383
|
-
remaining -= 255;
|
|
384
|
-
}
|
|
385
|
-
output[pos++] = remaining;
|
|
386
|
-
}
|
|
369
|
+
// Copy literals
|
|
370
|
+
for (let i = 0; i < literalLen; i++) {
|
|
371
|
+
output[pos++] = input[literalStart + i];
|
|
372
|
+
}
|
|
387
373
|
|
|
388
|
-
|
|
374
|
+
// Write offset (little-endian)
|
|
375
|
+
output[pos++] = offset & 0xff;
|
|
376
|
+
output[pos++] = (offset >> 8) & 0xff;
|
|
377
|
+
|
|
378
|
+
// Write extended match length
|
|
379
|
+
if (adjustedMatchLen >= 15) {
|
|
380
|
+
let remaining = adjustedMatchLen - 15;
|
|
381
|
+
while (remaining >= 255) {
|
|
382
|
+
output[pos++] = 255;
|
|
383
|
+
remaining -= 255;
|
|
384
|
+
}
|
|
385
|
+
output[pos++] = remaining;
|
|
389
386
|
}
|
|
390
387
|
|
|
391
|
-
|
|
388
|
+
return pos;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
392
|
* Writes final literals block (no match following).
|
|
393
393
|
* This is the last sequence in the compressed data.
|
|
394
394
|
* @param {Uint8Array} output - Output buffer
|
|
@@ -399,28 +399,28 @@ class MessageCompressor {
|
|
|
399
399
|
* @returns {number} New position
|
|
400
400
|
* @private
|
|
401
401
|
*/
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
// Copy literals
|
|
418
|
-
for (let i = 0; i < len; i++) {
|
|
419
|
-
output[pos++] = input[start + i];
|
|
420
|
-
}
|
|
402
|
+
_writeFinalLiterals(output, pos, input, start, len) {
|
|
403
|
+
// Token with literal length only (match length = 0)
|
|
404
|
+
const literalToken = Math.min(len, 15);
|
|
405
|
+
output[pos++] = literalToken << 4;
|
|
406
|
+
|
|
407
|
+
// Write extended literal length
|
|
408
|
+
if (len >= 15) {
|
|
409
|
+
let remaining = len - 15;
|
|
410
|
+
while (remaining >= 255) {
|
|
411
|
+
output[pos++] = 255;
|
|
412
|
+
remaining -= 255;
|
|
413
|
+
}
|
|
414
|
+
output[pos++] = remaining;
|
|
415
|
+
}
|
|
421
416
|
|
|
422
|
-
|
|
417
|
+
// Copy literals
|
|
418
|
+
for (let i = 0; i < len; i++) {
|
|
419
|
+
output[pos++] = input[start + i];
|
|
423
420
|
}
|
|
421
|
+
|
|
422
|
+
return pos;
|
|
423
|
+
}
|
|
424
424
|
}
|
|
425
425
|
|
|
426
426
|
/**
|
|
@@ -435,7 +435,7 @@ const defaultCompressor = new MessageCompressor();
|
|
|
435
435
|
* @returns {{ data: Uint8Array, compressed: boolean }} Result
|
|
436
436
|
*/
|
|
437
437
|
function compress(payload) {
|
|
438
|
-
|
|
438
|
+
return defaultCompressor.compress(payload);
|
|
439
439
|
}
|
|
440
440
|
|
|
441
441
|
/**
|
|
@@ -445,12 +445,12 @@ function compress(payload) {
|
|
|
445
445
|
* @returns {Uint8Array} Decompressed data
|
|
446
446
|
*/
|
|
447
447
|
function decompress(payload, wasCompressed) {
|
|
448
|
-
|
|
448
|
+
return defaultCompressor.decompress(payload, wasCompressed);
|
|
449
449
|
}
|
|
450
450
|
|
|
451
451
|
module.exports = {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
452
|
+
MessageCompressor,
|
|
453
|
+
compress,
|
|
454
|
+
decompress,
|
|
455
|
+
DEFAULT_CONFIG
|
|
456
456
|
};
|