@bcts/uniform-resources 1.0.0-alpha.12 → 1.0.0-alpha.14
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/dist/index.cjs +191 -213
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -249
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +13 -249
- package/dist/index.d.mts.map +1 -1
- package/dist/index.iife.js +192 -215
- package/dist/index.iife.js.map +1 -1
- package/dist/index.mjs +192 -200
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -5
- package/src/error.ts +11 -0
- package/src/fountain.ts +15 -35
- package/src/index.ts +5 -24
- package/src/multipart-decoder.ts +30 -36
- package/src/multipart-encoder.ts +6 -46
- package/src/utils.ts +7 -10
- package/src/xoshiro.ts +170 -76
package/src/xoshiro.ts
CHANGED
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
* for deterministic fragment selection in fountain codes.
|
|
6
6
|
*
|
|
7
7
|
* Reference: https://prng.di.unimi.it/
|
|
8
|
+
* BC-UR Reference: https://github.com/nicklockwood/fountain-codes
|
|
8
9
|
*/
|
|
9
10
|
|
|
11
|
+
import { sha256 } from "@bcts/crypto";
|
|
12
|
+
|
|
10
13
|
const MAX_UINT64 = BigInt("0xffffffffffffffff");
|
|
11
14
|
|
|
12
15
|
/**
|
|
@@ -28,25 +31,33 @@ export class Xoshiro256 {
|
|
|
28
31
|
private s: [bigint, bigint, bigint, bigint];
|
|
29
32
|
|
|
30
33
|
/**
|
|
31
|
-
* Creates a new Xoshiro256** instance from a seed.
|
|
34
|
+
* Creates a new Xoshiro256** instance from a 32-byte seed.
|
|
32
35
|
*
|
|
33
|
-
* The seed
|
|
34
|
-
*
|
|
36
|
+
* The seed must be exactly 32 bytes (256 bits). The bytes are interpreted
|
|
37
|
+
* using the BC-UR reference algorithm: each 8-byte chunk is read as
|
|
38
|
+
* big-endian then stored as little-endian for the state.
|
|
35
39
|
*
|
|
36
|
-
* @param seed - The seed bytes (
|
|
40
|
+
* @param seed - The seed bytes (must be exactly 32 bytes)
|
|
37
41
|
*/
|
|
38
42
|
constructor(seed: Uint8Array) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
43
|
+
if (seed.length !== 32) {
|
|
44
|
+
throw new Error(`Seed must be 32 bytes, got ${seed.length}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// BC-UR reference implementation:
|
|
48
|
+
// For each 8-byte chunk, read as big-endian u64, then convert to little-endian bytes
|
|
49
|
+
// This effectively swaps the byte order within each 8-byte segment
|
|
50
|
+
const s: [bigint, bigint, bigint, bigint] = [0n, 0n, 0n, 0n];
|
|
51
|
+
for (let i = 0; i < 4; i++) {
|
|
52
|
+
// Read 8 bytes as big-endian u64
|
|
53
|
+
let v = 0n;
|
|
54
|
+
for (let n = 0; n < 8; n++) {
|
|
55
|
+
v = (v << 8n) | BigInt(seed[8 * i + n] ?? 0);
|
|
56
|
+
}
|
|
57
|
+
s[i] = v;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this.s = s;
|
|
50
61
|
}
|
|
51
62
|
|
|
52
63
|
/**
|
|
@@ -59,47 +70,6 @@ export class Xoshiro256 {
|
|
|
59
70
|
return instance;
|
|
60
71
|
}
|
|
61
72
|
|
|
62
|
-
/**
|
|
63
|
-
* Simple hash function for seeding.
|
|
64
|
-
* This is a basic implementation - in production use SHA-256.
|
|
65
|
-
*/
|
|
66
|
-
private hashSeed(seed: Uint8Array): Uint8Array {
|
|
67
|
-
// Simple hash expansion using CRC32-like operations
|
|
68
|
-
const result = new Uint8Array(32);
|
|
69
|
-
|
|
70
|
-
if (seed.length === 0) {
|
|
71
|
-
return result;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Expand seed to 32 bytes using a simple mixing function
|
|
75
|
-
for (let i = 0; i < 32; i++) {
|
|
76
|
-
let hash = 0;
|
|
77
|
-
for (const byte of seed) {
|
|
78
|
-
hash = (hash * 31 + byte + i) >>> 0;
|
|
79
|
-
}
|
|
80
|
-
// Mix the hash further
|
|
81
|
-
hash ^= hash >>> 16;
|
|
82
|
-
hash = (hash * 0x85ebca6b) >>> 0;
|
|
83
|
-
hash ^= hash >>> 13;
|
|
84
|
-
hash = (hash * 0xc2b2ae35) >>> 0;
|
|
85
|
-
hash ^= hash >>> 16;
|
|
86
|
-
result[i] = hash & 0xff;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return result;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Converts 8 bytes to a 64-bit BigInt (little-endian).
|
|
94
|
-
*/
|
|
95
|
-
private bytesToBigInt(bytes: Uint8Array): bigint {
|
|
96
|
-
let result = 0n;
|
|
97
|
-
for (let i = 7; i >= 0; i--) {
|
|
98
|
-
result = (result << 8n) | BigInt(bytes[i] ?? 0);
|
|
99
|
-
}
|
|
100
|
-
return result;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
73
|
/**
|
|
104
74
|
* Generates the next 64-bit random value.
|
|
105
75
|
*/
|
|
@@ -121,19 +91,21 @@ export class Xoshiro256 {
|
|
|
121
91
|
|
|
122
92
|
/**
|
|
123
93
|
* Generates a random double in [0, 1).
|
|
94
|
+
* Matches BC-UR reference: self.next() as f64 / (u64::MAX as f64 + 1.0)
|
|
124
95
|
*/
|
|
125
96
|
nextDouble(): number {
|
|
126
|
-
// Use the upper 53 bits for double precision
|
|
127
97
|
const value = this.next();
|
|
128
|
-
|
|
98
|
+
// u64::MAX as f64 + 1.0 = 18446744073709551616.0
|
|
99
|
+
return Number(value) / 18446744073709551616.0;
|
|
129
100
|
}
|
|
130
101
|
|
|
131
102
|
/**
|
|
132
|
-
* Generates a random integer in [low, high).
|
|
103
|
+
* Generates a random integer in [low, high] (inclusive).
|
|
104
|
+
* Matches BC-UR reference: (self.next_double() * ((high - low + 1) as f64)) as u64 + low
|
|
133
105
|
*/
|
|
134
106
|
nextInt(low: number, high: number): number {
|
|
135
|
-
const range = high - low;
|
|
136
|
-
return
|
|
107
|
+
const range = high - low + 1;
|
|
108
|
+
return Math.floor(this.nextDouble() * range) + low;
|
|
137
109
|
}
|
|
138
110
|
|
|
139
111
|
/**
|
|
@@ -153,28 +125,150 @@ export class Xoshiro256 {
|
|
|
153
125
|
}
|
|
154
126
|
return result;
|
|
155
127
|
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Shuffles items by repeatedly picking random indices.
|
|
131
|
+
* Matches BC-UR reference implementation.
|
|
132
|
+
*/
|
|
133
|
+
shuffled<T>(items: T[]): T[] {
|
|
134
|
+
const source = [...items];
|
|
135
|
+
const shuffled: T[] = [];
|
|
136
|
+
while (source.length > 0) {
|
|
137
|
+
const index = this.nextInt(0, source.length - 1);
|
|
138
|
+
const item = source.splice(index, 1)[0];
|
|
139
|
+
if (item !== undefined) {
|
|
140
|
+
shuffled.push(item);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return shuffled;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Chooses the degree (number of fragments to mix) using a weighted sampler.
|
|
148
|
+
* Uses the robust soliton distribution with weights [1/1, 1/2, 1/3, ..., 1/n].
|
|
149
|
+
* Matches BC-UR reference implementation.
|
|
150
|
+
*/
|
|
151
|
+
chooseDegree(seqLen: number): number {
|
|
152
|
+
// Create weights: [1/1, 1/2, 1/3, ..., 1/seqLen]
|
|
153
|
+
const weights: number[] = [];
|
|
154
|
+
for (let i = 1; i <= seqLen; i++) {
|
|
155
|
+
weights.push(1.0 / i);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Use Vose's alias method for weighted sampling
|
|
159
|
+
const sampler = new WeightedSampler(weights);
|
|
160
|
+
return sampler.next(this) + 1; // 1-indexed degree
|
|
161
|
+
}
|
|
156
162
|
}
|
|
157
163
|
|
|
158
164
|
/**
|
|
159
|
-
*
|
|
165
|
+
* Weighted sampler using Vose's alias method.
|
|
166
|
+
* Allows O(1) sampling from a discrete probability distribution.
|
|
167
|
+
*/
|
|
168
|
+
class WeightedSampler {
|
|
169
|
+
private readonly aliases: number[];
|
|
170
|
+
private readonly probs: number[];
|
|
171
|
+
|
|
172
|
+
constructor(weights: number[]) {
|
|
173
|
+
const n = weights.length;
|
|
174
|
+
if (n === 0) {
|
|
175
|
+
throw new Error("Weights array cannot be empty");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Normalize weights
|
|
179
|
+
const sum = weights.reduce((a, b) => a + b, 0);
|
|
180
|
+
if (sum <= 0) {
|
|
181
|
+
throw new Error("Weights must sum to a positive value");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const normalized = weights.map((w) => (w * n) / sum);
|
|
185
|
+
|
|
186
|
+
// Initialize alias table
|
|
187
|
+
this.aliases = Array.from<number>({ length: n }).fill(0);
|
|
188
|
+
this.probs = Array.from<number>({ length: n }).fill(0);
|
|
189
|
+
|
|
190
|
+
// Partition into small and large
|
|
191
|
+
const small: number[] = [];
|
|
192
|
+
const large: number[] = [];
|
|
193
|
+
|
|
194
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
195
|
+
if (normalized[i] < 1.0) {
|
|
196
|
+
small.push(i);
|
|
197
|
+
} else {
|
|
198
|
+
large.push(i);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Build the alias table
|
|
203
|
+
while (small.length > 0 && large.length > 0) {
|
|
204
|
+
const a = small.pop();
|
|
205
|
+
const g = large.pop();
|
|
206
|
+
if (a === undefined || g === undefined) break;
|
|
207
|
+
this.probs[a] = normalized[a] ?? 0;
|
|
208
|
+
this.aliases[a] = g;
|
|
209
|
+
const normalizedG = normalized[g] ?? 0;
|
|
210
|
+
const normalizedA = normalized[a] ?? 0;
|
|
211
|
+
normalized[g] = normalizedG + normalizedA - 1.0;
|
|
212
|
+
if (normalized[g] !== undefined && normalized[g] < 1.0) {
|
|
213
|
+
small.push(g);
|
|
214
|
+
} else {
|
|
215
|
+
large.push(g);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
while (large.length > 0) {
|
|
220
|
+
const g = large.pop();
|
|
221
|
+
if (g === undefined) break;
|
|
222
|
+
this.probs[g] = 1.0;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
while (small.length > 0) {
|
|
226
|
+
const a = small.pop();
|
|
227
|
+
if (a === undefined) break;
|
|
228
|
+
this.probs[a] = 1.0;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Sample from the distribution.
|
|
234
|
+
*/
|
|
235
|
+
next(rng: Xoshiro256): number {
|
|
236
|
+
const r1 = rng.nextDouble();
|
|
237
|
+
const r2 = rng.nextDouble();
|
|
238
|
+
const n = this.probs.length;
|
|
239
|
+
const i = Math.floor(n * r1);
|
|
240
|
+
if (r2 < this.probs[i]) {
|
|
241
|
+
return i;
|
|
242
|
+
} else {
|
|
243
|
+
return this.aliases[i];
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Creates a Xoshiro256 PRNG instance from message checksum and sequence number.
|
|
250
|
+
*
|
|
251
|
+
* This creates an 8-byte seed by concatenating seqNum and checksum (both in
|
|
252
|
+
* big-endian), then hashes it with SHA-256 to get the 32-byte seed for Xoshiro.
|
|
160
253
|
*
|
|
161
|
-
* This
|
|
162
|
-
* for a given message and part number.
|
|
254
|
+
* This matches the BC-UR reference implementation.
|
|
163
255
|
*/
|
|
164
256
|
export function createSeed(checksum: number, seqNum: number): Uint8Array {
|
|
165
|
-
|
|
257
|
+
// Create 8-byte seed: seqNum (big-endian) || checksum (big-endian)
|
|
258
|
+
const seed8 = new Uint8Array(8);
|
|
166
259
|
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
260
|
+
// seqNum in big-endian (bytes 0-3)
|
|
261
|
+
seed8[0] = (seqNum >>> 24) & 0xff;
|
|
262
|
+
seed8[1] = (seqNum >>> 16) & 0xff;
|
|
263
|
+
seed8[2] = (seqNum >>> 8) & 0xff;
|
|
264
|
+
seed8[3] = seqNum & 0xff;
|
|
172
265
|
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
266
|
+
// checksum in big-endian (bytes 4-7)
|
|
267
|
+
seed8[4] = (checksum >>> 24) & 0xff;
|
|
268
|
+
seed8[5] = (checksum >>> 16) & 0xff;
|
|
269
|
+
seed8[6] = (checksum >>> 8) & 0xff;
|
|
270
|
+
seed8[7] = checksum & 0xff;
|
|
178
271
|
|
|
179
|
-
|
|
272
|
+
// Hash with SHA-256 to get 32 bytes
|
|
273
|
+
return sha256(seed8);
|
|
180
274
|
}
|