-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathutils.js
More file actions
145 lines (125 loc) · 4.61 KB
/
Copy pathutils.js
File metadata and controls
145 lines (125 loc) · 4.61 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
const assert = require("assert");
const crypto = require("crypto");
const BYTES_PER_ROUND = 32; // one HMAC-SHA256 digest
const BYTES_PER_FLOAT = 4;
function randomUUID() {
return crypto.randomUUID();
}
function sha256(input = randomUUID()) {
return crypto.createHash("sha256").update(input).digest("hex");
}
function md5(input = randomUUID()) {
return crypto.createHash("md5").update(input).digest("hex");
}
// Accepts a safe integer, or its canonical decimal string form because values
// read back from a database or a query string often arrive as strings.
// Anything else (floats, NaN, "1e3", objects) throws.
function toInteger(value, name = "value") {
if (typeof value === "string" && /^-?\d+$/.test(value)) value = Number(value);
assert(Number.isSafeInteger(value), `${name} must be a safe integer`);
return value;
}
function assertSeed(value, name = "seed") {
assert(
typeof value === "string" && value.length > 0,
`${name} must be a non-empty string`
);
}
// Normalises and validates a provable state. Returns a new object; the input
// is never mutated. Unknown keys (ids, timestamps) are carried through.
// `serverHash` is always derived from `serverSeed`: a supplied value is never
// trusted, so the commitment can not drift from the seed it commits to.
function defaults(state = {}) {
assert(state && typeof state === "object", "state must be an object");
const result = {
...state,
serverSeed: state.serverSeed ?? sha256(),
clientSeed: state.clientSeed ?? md5(),
nonce: toInteger(state.nonce ?? 0, "nonce"),
cursor: toInteger(state.cursor ?? 0, "cursor"),
};
assertSeed(result.serverSeed, "serverSeed");
assertSeed(result.clientSeed, "clientSeed");
assert(result.nonce >= 0, "nonce must be 0 or more");
assert(result.cursor >= 0, "cursor must be 0 or more");
result.serverHash = sha256(result.serverSeed);
return result;
}
// Yields bytes from HMAC-SHA256(key = serverSeed, message = `clientSeed:nonce:round`).
// Each round yields 32 bytes; `cursor` is the byte offset to start from.
// The nonce is fixed for the life of the generator: open a new one per draw.
function* ByteGenerator({ serverSeed, clientSeed, nonce, cursor = 0 }) {
let currentRound = Math.floor(cursor / BYTES_PER_ROUND);
let currentRoundCursor = cursor - currentRound * BYTES_PER_ROUND;
while (true) {
const hmac = crypto.createHmac("sha256", serverSeed);
hmac.update(`${clientSeed}:${nonce}:${currentRound}`);
const buffer = hmac.digest();
while (currentRoundCursor < BYTES_PER_ROUND) {
yield Number(buffer[currentRoundCursor]);
currentRoundCursor += 1;
}
currentRoundCursor = 0;
currentRound += 1;
}
}
// Four bytes become a float in [0, 1) with 32 bits of precision:
// b0/256 + b1/256^2 + b2/256^3 + b3/256^4
function bytesToFloat(bytes) {
return bytes.reduce((result, value, i) => {
const divider = 256 ** (i + 1);
return result + value / divider;
}, 0);
}
// Maps a float in [0, 1) onto the `max` integers starting at `min`, i.e. the
// range [min, min + max - 1]. `max` is a range size, not an upper bound.
// Slightly biased when `max` does not divide 2^32; `ints` is the unbiased form.
function floatToInt(val, max, min = 0) {
return Math.floor(min + val * max);
}
function* FloatGenerator(rng, count) {
for (let i = 0; i < count; i++) {
const bytes = [];
for (let j = 0; j < BYTES_PER_FLOAT; j++) {
bytes.push(rng.next().value);
}
yield bytesToFloat(bytes);
}
}
function floats(rng, count) {
return [...FloatGenerator(rng, count)];
}
const TWO_32 = 2 ** 32;
// Unbiased integers in [min, min + max - 1] (Lemire's multiply-shift with
// rejection). Each draw reads 4 bytes as a big-endian u32 and takes the high
// 32 bits of u * max, which equals floatToInt(bytesToFloat(bytes), max, min).
// The few u32 values that would over-represent some results are rejected and
// the next 4 bytes are drawn instead, so every result is exactly equally likely.
function ints(rng, count, max, min = 0) {
assert(Number.isSafeInteger(max) && max >= 1 && max <= TWO_32, "max must be 1 to 2^32");
const range = BigInt(max);
const threshold = BigInt(TWO_32 % max);
const result = [];
while (result.length < count) {
let u = 0n;
for (let j = 0; j < BYTES_PER_FLOAT; j++) u = (u << 8n) | BigInt(rng.next().value);
const m = u * range;
if ((m & 0xffffffffn) < threshold) continue; // biased slot: draw again
result.push(min + Number(m >> 32n));
}
return result;
}
module.exports = {
floats,
bytesToFloat,
ByteGenerator,
sha256,
md5,
ints,
FloatGenerator,
floatToInt,
defaults,
toInteger,
assertSeed,
randomUUID,
};