rapier-html 1.1.101 → 1.1.104

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.
@@ -1,13 +1,33 @@
1
1
  // SPDX-License-Identifier: MIT
2
- import {_rapierValidLedgerRecord, _rapierRecordSplices, _rapierTransformSplices, _rapierRecordMetadata, _rapierValidMetadata, _rapierTransformMetadata, _rapierReplayMetadata, _rapierHistoryEffects} from './journal-records.mjs';
2
+ import {_rapierPrepareHistoryRecords, _rapierValidMetadata, _rapierTransformMetadata} from './journal-records.mjs';
3
3
  import {pictureHistorySplices} from './history.mjs';
4
+ import {HistorySource} from './inverse-source.mjs';
4
5
  import {authoredHistory} from './transport.mjs';
5
- import {textRoot, rootAfter, sha256} from './hash.mjs';
6
- import {canonicalJSON, plainData, documentText, exactKeys, nonnegative, fault} from './data.mjs';
6
+ import {textRoot, rootAfter, sha256, sha256Parts, createSHA256} from './hash.mjs';
7
+ import {canonicalJSON, canonicalParts, plainData, documentText, utf8Length, exactKeys, nonnegative, fault} from './data.mjs';
7
8
  export {textRoot, rootAfter};
8
- const digest = value => sha256(canonicalJSON(value));
9
+ const digest = value => sha256Parts(canonicalParts(value));
9
10
  const rootShape = value => typeof value === 'string' && /^\d+:[a-z0-9]+:[a-z0-9]+$/.test(value);
10
- export function readLedger(input, expectedText, expectedMetadata) {
11
+ // Structural/history admission is shared by the reader and the producer. The producer
12
+ // proves source replay by reversing exact admitted splices; it need not replay its output.
13
+ function* checkedRecords(ledger, prepared) {
14
+ if (!prepared.ok || ledger.records.length !== prepared.records.length) throw fault('record chain differs');
15
+ let root = ledger.start.root, revision = ledger.start.revision;
16
+ for (let index = 0; index < prepared.records.length; index++) {
17
+ const record = ledger.records[index];
18
+ if (!Array.isArray(record.splices) || !prepared.validRecord(index,
19
+ ledger.documentAuthority, 64 * 1024 * 1024, utf8Length, !ledger.complete) ||
20
+ record.transaction.baseRevision < revision || (record.transaction.baseRevision !== revision && ledger.complete) ||
21
+ (record.transaction.baseRevision === revision && record.beforeHash !== root)) throw fault('record chain differs');
22
+ if (record.transaction.baseRevision !== revision) root = record.beforeHash;
23
+ for (const row of record.splices) root = rootAfter(root, row);
24
+ if (root !== record.afterHash) throw fault('record root differs');
25
+ revision = record.transaction.revision;
26
+ yield record;
27
+ }
28
+ if (ledger.head.revision !== revision || ledger.head.root !== root) throw fault('history does not describe this document');
29
+ }
30
+ function readLedgerState(input, expectedText, expectedMetadata) {
11
31
  const ledger = plainData(input);
12
32
  if (!exactKeys(ledger, ['format','documentAuthority','start','records','head','complete','sha256']) || ledger.format !== 'rapier-ledger/2' ||
13
33
  typeof ledger.documentAuthority !== 'string' || !ledger.documentAuthority || ledger.documentAuthority.length > 256 ||
@@ -19,102 +39,195 @@ export function readLedger(input, expectedText, expectedMetadata) {
19
39
  if (!_rapierValidMetadata(ledger.start.metadata) || !_rapierValidMetadata(ledger.head.metadata)) throw fault('invalid document metadata');
20
40
  if (!nonnegative(revision) || !rootShape(root) || (revision === 0 && root !== textRoot(text)) || ledger.start.sha256 !== sha256(text) ||
21
41
  (ledger.complete && revision !== 0)) throw fault('invalid start');
22
- const earlier = [];
23
- for (const record of ledger.records) {
24
- if (!Array.isArray(record.splices) || !_rapierValidLedgerRecord(record, earlier.at(-1) || null, earlier,
25
- ledger.documentAuthority, 64 * 1024 * 1024, text => new TextEncoder().encode(text).length, !ledger.complete) ||
26
- record.transaction.baseRevision < revision || (record.transaction.baseRevision !== revision && ledger.complete) ||
27
- (record.transaction.baseRevision === revision && record.beforeHash !== root)) throw fault('record chain differs');
28
- // A pruned cancelling pair leaves a stated revision gap, just as in the editor's restore reader.
29
- if (record.transaction.baseRevision !== revision) root = record.beforeHash;
30
- const next = _rapierTransformSplices(text, record.splices);
42
+ const prepared = _rapierPrepareHistoryRecords(ledger.records);
43
+ if (!prepared.ok) throw fault('record chain differs');
44
+ let source = new HistorySource(text), bytes = utf8Length(text);
45
+ for (const record of checkedRecords(ledger, prepared)) {
46
+ const next = source.apply(record.splices);
31
47
  if (next === null) throw fault('splice does not replay');
32
- text = documentText(next);
33
- for (const row of record.splices) root = rootAfter(root, row);
34
- if (root !== record.afterHash) throw fault('record root differs');
35
- revision = record.transaction.revision; earlier.push(record);
48
+ source = next;
49
+ for (const row of record.splices) bytes += utf8Length(row.inserted) - utf8Length(row.removed);
50
+ if (bytes > 64 * 1024 * 1024) throw fault('document exceeds the editor text limit');
51
+ root = record.afterHash; revision = record.transaction.revision;
36
52
  }
53
+ text = source.slice();
37
54
  if (ledger.records.some(row => Object.hasOwn(row, 'authored'))) {
38
55
  try { authoredHistory(ledger.start.text, ledger.records); } catch (_) { throw fault('authored origin differs'); }
39
56
  }
40
- const metadata = _rapierReplayMetadata(ledger.start.metadata, ledger.records);
57
+ const metadata = prepared.replayMetadata(ledger.start.metadata);
41
58
  if (!metadata || canonicalJSON(metadata) !== canonicalJSON(ledger.head.metadata) ||
42
59
  (expectedMetadata !== undefined && (!_rapierValidMetadata(expectedMetadata) || canonicalJSON(metadata) !== canonicalJSON(expectedMetadata))))
43
60
  throw fault('metadata does not describe this document');
44
61
  if (ledger.head.revision !== revision || ledger.head.root !== root || ledger.head.sha256 !== sha256(text) ||
45
62
  (expectedText !== undefined && text !== expectedText)) throw fault('history does not describe this document');
46
- return {ledger, text, metadata, root, revision, complete: ledger.complete};
63
+ return {value: {ledger, text, metadata, root, revision, complete: ledger.complete}, prepared, source};
64
+ }
65
+ export function readLedger(input, expectedText, expectedMetadata) {
66
+ return readLedgerState(input, expectedText, expectedMetadata).value;
47
67
  }
48
- export function exportLedger({text, metadata, records, documentAuthority, revision, root, complete = false, earlier = records} = {}) {
49
- documentText(text);
68
+ function* checksum(parts) {
69
+ const hash = createSHA256();
70
+ for (const part of parts) { hash.update(part); yield; }
71
+ return hash.digest();
72
+ }
73
+ function* sourceFacts(text) {
74
+ if (typeof text !== 'string') throw fault('document is not whole-character text');
75
+ const hash = createSHA256();
76
+ let fnv = 0x811c9dc5, a = 1, b = 0, bytes = 0;
77
+ for (let at = 0; at < text.length;) {
78
+ const start = at, end = Math.min(text.length, at + 65536);
79
+ for (; at < end;) {
80
+ const stop = Math.min(end, at + 4096);
81
+ for (; at < stop; at++) {
82
+ const code = text.charCodeAt(at);
83
+ fnv = Math.imul(fnv ^ code, 16777619); a += code; b += a;
84
+ if (code < 0x80) bytes++;
85
+ else if (code < 0x800) bytes += 2;
86
+ else if ((code & 0xfc00) === 0xd800) {
87
+ if ((text.charCodeAt(at + 1) & 0xfc00) !== 0xdc00) throw fault('document is not whole-character text');
88
+ bytes += 3;
89
+ } else if ((code & 0xfc00) === 0xdc00) {
90
+ if (!at || (text.charCodeAt(at - 1) & 0xfc00) !== 0xd800) throw fault('document is not whole-character text');
91
+ bytes++;
92
+ } else bytes += 3;
93
+ }
94
+ a %= 65521; b %= 65521;
95
+ }
96
+ if (bytes > 64 * 1024 * 1024) throw fault('document exceeds the editor text limit');
97
+ hash.update(text.slice(start, end)); yield;
98
+ }
99
+ return {utf8Bytes: bytes, sha256: hash.digest(), contentRoot: text.length + ':' +
100
+ (fnv >>> 0).toString(36) + ':' + (((b << 16) | a) >>> 0).toString(36)};
101
+ }
102
+ function* produceLedger({text, metadata, records, documentAuthority, revision, root, complete = false, earlier = records} = {}) {
50
103
  if (!_rapierValidMetadata(metadata)) throw fault('document metadata required');
104
+ // Metadata has two admitted scalar fields. Keep its insertion order in the
105
+ // emitted envelope while capturing it before the first cooperative boundary.
106
+ metadata = {...metadata};
51
107
  if (!Array.isArray(records) || !Array.isArray(earlier)) throw fault('records required');
52
108
  // Inspect before touching properties: exporting must not call accessors on an alleged record.
53
- records = plainData(records); earlier = plainData(earlier);
54
- const expanded = records.map(record => {
55
- const rows = _rapierRecordSplices(record, earlier), effect = _rapierRecordMetadata(record, earlier);
109
+ const prepared = _rapierPrepareHistoryRecords(records, earlier);
110
+ if (!prepared.ok) throw fault('navigation target was not retained');
111
+ const headFacts = yield* sourceFacts(text);
112
+ let bytes = headFacts.utf8Bytes;
113
+ const expanded = [];
114
+ for (const {record, splices: rows, metadata: effect} of prepared.rows) {
56
115
  if (!rows || effect === undefined) throw fault('navigation target was not retained');
57
- return {transaction: record.transaction, splices: rows.map(row => ({pos: row.pos, removed: row.removed, inserted: row.inserted})),
58
- beforeHash: record.beforeHash, afterHash: record.afterHash, ...(Object.hasOwn(record, 'authored') ? {authored: record.authored} : {}), ...(effect ? {metadata: effect} : {}),
59
- ...(record.changeSet && Object.hasOwn(record.changeSet, 'label') ? {changeSet: {label: record.changeSet.label}} : {}),
60
- ...(record.derivedCommentIndex == null ? {} : {derivedCommentIndex: record.derivedCommentIndex})};
61
- });
62
- let startText = text, startMetadata = {...metadata};
116
+ expanded.push({transaction: plainData(record.transaction), splices: rows.map(row => ({pos: row.pos, removed: row.removed, inserted: row.inserted})),
117
+ beforeHash: record.beforeHash, afterHash: record.afterHash, ...(Object.hasOwn(record, 'authored') ? {authored: plainData(record.authored)} : {}), ...(effect ? {metadata: plainData(effect)} : {}),
118
+ ...(record.changeSet && Object.hasOwn(record.changeSet, 'label') ? {changeSet: {label: typeof record.changeSet.label === 'string' ? record.changeSet.label : plainData(record.changeSet.label)}} : {}),
119
+ ...(record.derivedCommentIndex == null ? {} : {derivedCommentIndex: typeof record.derivedCommentIndex === 'string' ? record.derivedCommentIndex : plainData(record.derivedCommentIndex)})});
120
+ yield;
121
+ }
122
+ const headBytes = bytes;
123
+ let startSource = new HistorySource(text), startMetadata = {...metadata};
63
124
  for (let i = expanded.length - 1; i >= 0; i--) {
64
- startText = _rapierTransformSplices(startText, expanded[i].splices, true);
125
+ startSource = startSource.apply(expanded[i].splices, true);
65
126
  startMetadata = _rapierTransformMetadata(startMetadata, expanded[i].metadata, true);
66
- if (startText === null || !startMetadata) throw fault('history does not reverse from this document');
127
+ if (startSource === null || !startMetadata) throw fault('history does not reverse from this document');
128
+ // Whole scalars and exact splice boundaries were just checked. Count the changed
129
+ // bytes, rather than re-encoding every reconstructed whole-document revision.
130
+ for (const row of expanded[i].splices) bytes += utf8Length(row.removed) - utf8Length(row.inserted);
131
+ if (bytes > 64 * 1024 * 1024) throw fault('document exceeds the editor text limit');
132
+ yield;
67
133
  }
134
+ const startText = startSource.slice();
135
+ const unchangedText = expanded.every(row => !row.splices.length);
136
+ const contentRoot = headFacts.contentRoot;
68
137
  revision ??= expanded.at(-1)?.transaction.revision ?? 0;
69
- root ??= expanded.at(-1)?.afterHash ?? textRoot(text);
138
+ root ??= expanded.at(-1)?.afterHash ?? contentRoot;
70
139
  documentAuthority ??= expanded[0]?.transaction.documentAuthority;
71
- const startRevision = expanded[0]?.transaction.baseRevision ?? revision;
140
+ const startRevision = expanded[0]?.transaction.baseRevision ?? revision, startRoot = expanded[0]?.beforeHash ?? root;
141
+ if (typeof documentAuthority !== 'string' || !documentAuthority || documentAuthority.length > 256) throw fault('unsupported or invalid envelope');
142
+ if (!nonnegative(startRevision) || !rootShape(startRoot)) throw fault('invalid start');
143
+ const startFacts = unchangedText ? headFacts : yield* sourceFacts(startText);
144
+ if (startRevision === 0 && startRoot !== startFacts.contentRoot) throw fault('invalid start');
145
+ const headSHA = headFacts.sha256, startSHA = startFacts.sha256;
72
146
  const payload = {format: 'rapier-ledger/2', documentAuthority,
73
- start: {text: startText, revision: startRevision, root: expanded[0]?.beforeHash ?? root, sha256: sha256(startText), metadata: startMetadata},
74
- records: expanded, head: {revision, root, sha256: sha256(text), metadata: {...metadata}}, complete: complete === true && startRevision === 0 && expanded.every((row, i) => !i || row.transaction.baseRevision === expanded[i - 1].transaction.revision)};
75
- const ledger = {...payload, sha256: digest(payload)};
76
- readLedger(ledger, text, metadata); return ledger;
147
+ start: {text: startText, revision: startRevision, root: startRoot, sha256: startSHA, metadata: startMetadata},
148
+ records: expanded, head: {revision, root, sha256: headSHA, metadata: {...metadata}}, complete: complete === true && startRevision === 0 && expanded.every((row, i) => !i || row.transaction.baseRevision === expanded[i - 1].transaction.revision)};
149
+ // Expanded rows are the exact canonical projection of this owned preparation.
150
+ // Keep its navigation/effect proof instead of admitting another copied graph.
151
+ for (const _record of checkedRecords(payload, prepared)) yield;
152
+ if (expanded.some(row => Object.hasOwn(row, 'authored'))) {
153
+ try { authoredHistory(startText, expanded); } catch (_) { throw fault('authored origin differs'); }
154
+ }
155
+ const replayedMetadata = prepared.replayMetadata(startMetadata);
156
+ if (!replayedMetadata || canonicalJSON(replayedMetadata) !== canonicalJSON(metadata)) throw fault('metadata does not describe this document');
157
+ const ledger = {...payload, sha256: yield* checksum(canonicalParts(payload))};
158
+ return {ledger, contentRoot, utf8Bytes: headBytes};
159
+ }
160
+ function finish(producer) {
161
+ let step;
162
+ do { step = producer.next(); } while (!step.done);
163
+ return step.value;
164
+ }
165
+ export function exportLedger(input) { return finish(produceLedger(input)).ledger; }
166
+ // Both synchronous public export and cooperative recovery use the same producer.
167
+ // Its returned facts were computed while admitting/hashing the exact head, never supplied.
168
+ export function prepareLedger(input) { return finish(produceLedger(input)); }
169
+ export async function prepareLedgerAsync(input, {yield: yieldWork, signal} = {}) {
170
+ const producer = produceLedger(input);
171
+ let step;
172
+ do {
173
+ signal?.throwIfAborted();
174
+ step = producer.next();
175
+ if (!step.done && yieldWork) {
176
+ // The caller owns scheduling and its clock; ledger admission reads neither.
177
+ const pending = yieldWork();
178
+ if (pending) await pending;
179
+ }
180
+ } while (!step.done);
181
+ signal?.throwIfAborted();
182
+ return step.value;
77
183
  }
78
184
  // Derived navigation, never a second history. A target older than start is intentionally unavailable.
79
185
  export function historyEnvelope(input, segmentIdentity = []) {
80
- const {ledger, text, metadata} = readLedger(input), ids = new Set(ledger.records.map(row => row.transaction.id));
81
- const pictures = _rapierHistoryEffects(ledger.records).pictures;
186
+ const {value: {ledger, text, metadata}, prepared, source} = readLedgerState(input);
187
+ const {byId, effects: {pictures}} = prepared;
82
188
  let branch = [], cursor = 0;
83
189
  for (const {transaction: tx} of ledger.records) {
84
190
  if (pictures.has(tx.id)) continue;
85
191
  const target = tx.reverts || tx.reapplies;
86
192
  if (target) {
87
- if (!ids.has(target)) continue;
193
+ if (!byId.has(target)) continue;
88
194
  if (tx.reverts && branch[cursor - 1] === target) cursor--;
89
195
  else if (tx.reapplies && branch[cursor] === target) cursor++;
90
196
  else throw fault('invalid navigation order');
91
- } else { branch = branch.slice(0, cursor); branch.push(tx.id); cursor = branch.length; }
197
+ } else { branch.length = cursor; branch.push(tx.id); cursor = branch.length; }
92
198
  }
93
- const byId = new Map(ledger.records.map(row => [row.transaction.id, row]));
94
199
  const step = (source, id, redo) => {
95
- const target = byId.get(id), folded = pictureHistorySplices({source, records: ledger.records}, target, redo);
96
- return _rapierTransformSplices(source, folded || target.splices, !folded && !redo);
200
+ const target = byId.get(id), folded = pictures.size
201
+ ? pictureHistorySplices({source: source.slice(), records: prepared.records}, target, redo) : null;
202
+ return source.apply(folded || target.splices, !folded && !redo);
97
203
  };
98
- let traversed = text, traversedMetadata = {...metadata};
204
+ let traversed = source, traversedMetadata = {...metadata};
99
205
  for (let i = cursor - 1; i >= 0; i--) {
100
206
  traversed = step(traversed, branch[i], false);
101
207
  traversedMetadata = _rapierTransformMetadata(traversedMetadata, byId.get(branch[i]).metadata, true);
102
208
  if (traversed === null || !traversedMetadata) throw fault('undo branch does not reverse');
103
209
  }
104
- traversed = text; traversedMetadata = {...metadata};
210
+ if (ledger.complete) {
211
+ if (ledger.records.length && !branch.length) throw fault('complete history lost its branch');
212
+ // Literal admitted splices are reversible. Proving the actual head back to
213
+ // the exact start also proves its forward active branch, without replaying it.
214
+ if (traversed.length !== ledger.start.text.length || !traversed.matches(0, ledger.start.text) ||
215
+ canonicalJSON(traversedMetadata) !== canonicalJSON(ledger.start.metadata)) throw fault('complete branch differs');
216
+ }
217
+ traversed = source; traversedMetadata = {...metadata};
105
218
  for (let i = cursor; i < branch.length; i++) {
106
219
  traversed = step(traversed, branch[i], true);
107
220
  traversedMetadata = _rapierTransformMetadata(traversedMetadata, byId.get(branch[i]).metadata);
108
221
  if (traversed === null || !traversedMetadata) throw fault('redo branch does not replay');
109
222
  }
110
- if (ledger.complete) {
111
- if (ledger.records.length && !branch.length) throw fault('complete history lost its branch');
112
- let active = ledger.start.text, activeMetadata = {...ledger.start.metadata};
113
- if (!cursor && (active !== text || canonicalJSON(activeMetadata) !== canonicalJSON(metadata))) throw fault('complete history start differs');
223
+ // Picture folding may relocate finished definitions while navigating. Keep its
224
+ // distinct forward proof; the literal-only case above needs no second traversal.
225
+ if (ledger.complete && pictures.size) {
226
+ let active = new HistorySource(ledger.start.text), activeMetadata = {...ledger.start.metadata};
114
227
  for (let i = 0; i < branch.length; i++) {
115
228
  active = step(active, branch[i], true);
116
229
  activeMetadata = _rapierTransformMetadata(activeMetadata, byId.get(branch[i]).metadata);
117
- if (active === null || !activeMetadata || (i + 1 === cursor && (active !== text || canonicalJSON(activeMetadata) !== canonicalJSON(metadata)))) throw fault('complete branch differs');
230
+ if (active === null || !activeMetadata || (i + 1 === cursor && (active.length !== text.length || !active.matches(0, text) || canonicalJSON(activeMetadata) !== canonicalJSON(metadata)))) throw fault('complete branch differs');
118
231
  }
119
232
  }
120
233
  return {schemaVersion: 5, documentAuthority: ledger.documentAuthority, documentRevision: ledger.head.revision,
@@ -1,12 +1,20 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  // The editor's path-dependent root, plus synchronous SHA-256 for file readers (including offline wrappers).
3
3
  export function textRoot(text) {
4
- let fnv = 0x811c9dc5, a = 1, b = 0;
5
- for (let i = 0; i < text.length; i++) {
6
- const c = text.charCodeAt(i);
7
- fnv = Math.imul(fnv ^ c, 16777619); a = (a + c) % 65521; b = (b + a) % 65521;
4
+ return textRootParts([text]);
5
+ }
6
+ // The same UTF-16 root over shared source pieces, without joining a historical body.
7
+ export function textRootParts(parts) {
8
+ let fnv = 0x811c9dc5, a = 1, b = 0, length = 0;
9
+ for (const text of parts) {
10
+ length += text.length;
11
+ for (let i = 0; i < text.length;) {
12
+ const end = Math.min(text.length, i + 4096);
13
+ for (; i < end; i++) { const c = text.charCodeAt(i); fnv = Math.imul(fnv ^ c, 16777619); a += c; b += a; }
14
+ a %= 65521; b %= 65521;
15
+ }
8
16
  }
9
- return text.length + ':' + (fnv >>> 0).toString(36) + ':' + (((b << 16) | a) >>> 0).toString(36);
17
+ return length + ':' + (fnv >>> 0).toString(36) + ':' + (((b << 16) | a) >>> 0).toString(36);
10
18
  }
11
19
  export function rootAfter(root, row) {
12
20
  return textRoot(String(root || '') + ':' + row.pos + ':' + textRoot(row.removed) + ':' + textRoot(row.inserted));
@@ -22,26 +30,75 @@ const K = new Uint32Array([
22
30
  0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2,
23
31
  ]);
24
32
  const rotate = (x, n) => (x >>> n) | (x << (32 - n));
25
- export function sha256(text) {
26
- const bytes = new TextEncoder().encode(text);
27
- const padded = new Uint8Array(Math.ceil((bytes.length + 9) / 64) * 64);
28
- padded.set(bytes); padded[bytes.length] = 0x80;
29
- const data = new DataView(padded.buffer), bits = bytes.length * 8;
30
- data.setUint32(padded.length - 8, Math.floor(bits / 0x100000000)); data.setUint32(padded.length - 4, bits >>> 0);
31
- const h = new Uint32Array([0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19]), w = new Uint32Array(64);
32
- for (let offset = 0; offset < padded.length; offset += 64) {
33
- for (let i = 0; i < 16; i++) w[i] = data.getUint32(offset + i * 4);
33
+ // A bounded encoder and one compression block. No full UTF-8 or padded-input copy.
34
+ // Updates retain a trailing high surrogate so an arbitrary string boundary stays exact.
35
+ export function createSHA256() {
36
+ const h = new Uint32Array([0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19]);
37
+ const w = new Uint32Array(64), block = new Uint8Array(64), encoded = new Uint8Array(65536);
38
+ const encoder = new TextEncoder();
39
+ let used = 0, bytes = 0, high = '', finished = false;
40
+ const compress = (input, offset) => {
41
+ for (let i = 0; i < 16; i++) {
42
+ const at = offset + i * 4;
43
+ w[i] = (input[at] << 24) | (input[at + 1] << 16) | (input[at + 2] << 8) | input[at + 3];
44
+ }
34
45
  for (let i = 16; i < 64; i++) {
35
46
  const x = w[i - 15], y = w[i - 2];
36
47
  w[i] = w[i - 16] + (rotate(x, 7) ^ rotate(x, 18) ^ (x >>> 3)) + w[i - 7] + (rotate(y, 17) ^ rotate(y, 19) ^ (y >>> 10));
37
48
  }
38
- let [a,b,c,d,e,f,g,k] = h;
49
+ let a=h[0], b=h[1], c=h[2], d=h[3], e=h[4], f=h[5], g=h[6], k=h[7];
39
50
  for (let i = 0; i < 64; i++) {
40
51
  const t1 = (k + (rotate(e, 6) ^ rotate(e, 11) ^ rotate(e, 25)) + ((e & f) ^ (~e & g)) + K[i] + w[i]) >>> 0;
41
52
  const t2 = ((rotate(a, 2) ^ rotate(a, 13) ^ rotate(a, 22)) + ((a & b) ^ (a & c) ^ (b & c))) >>> 0;
42
53
  k = g; g = f; f = e; e = (d + t1) >>> 0; d = c; c = b; b = a; a = (t1 + t2) >>> 0;
43
54
  }
44
- for (const [i, value] of [a,b,c,d,e,f,g,k].entries()) h[i] += value;
45
- }
46
- return [...h].map(x => x.toString(16).padStart(8, '0')).join('');
55
+ h[0]+=a; h[1]+=b; h[2]+=c; h[3]+=d; h[4]+=e; h[5]+=f; h[6]+=g; h[7]+=k;
56
+ };
57
+ const feed = (input, size) => {
58
+ bytes += size;
59
+ let at = 0;
60
+ if (used) {
61
+ const take = Math.min(64 - used, size);
62
+ block.set(input.subarray(0, take), used); used += take; at = take;
63
+ if (used === 64) { compress(block, 0); used = 0; }
64
+ }
65
+ for (; at + 64 <= size; at += 64) compress(input, at);
66
+ if (at < size) { block.set(input.subarray(at, size), 0); used = size - at; }
67
+ };
68
+ const write = text => {
69
+ for (let at = 0; at < text.length;) {
70
+ // Bound both the substring and byte allocation; encodeInto never cuts a scalar.
71
+ let end = Math.min(text.length, at + 32768);
72
+ if (end < text.length && (text.charCodeAt(end - 1) & 0xfc00) === 0xd800 && (text.charCodeAt(end) & 0xfc00) === 0xdc00) end--;
73
+ const part = encoder.encodeInto(text.slice(at, end), encoded);
74
+ feed(encoded, part.written); at += part.read;
75
+ }
76
+ };
77
+ return {
78
+ update(value) {
79
+ if (finished) throw new Error('SHA-256 is finished');
80
+ let text = high + String(value === undefined ? '' : value); high = '';
81
+ if (text.length && (text.charCodeAt(text.length - 1) & 0xfc00) === 0xd800) { high = text.slice(-1); text = text.slice(0, -1); }
82
+ write(text); return this;
83
+ },
84
+ digest() {
85
+ if (finished) throw new Error('SHA-256 is finished');
86
+ if (high) { write(high); high = ''; }
87
+ finished = true;
88
+ const bits = bytes * 8;
89
+ block[used++] = 0x80; block.fill(0, used);
90
+ if (used > 56) { compress(block, 0); block.fill(0); }
91
+ const view = new DataView(block.buffer);
92
+ view.setUint32(56, Math.floor(bits / 0x100000000)); view.setUint32(60, bits >>> 0);
93
+ compress(block, 0);
94
+ return Array.from(h, x => x.toString(16).padStart(8, '0')).join('');
95
+ },
96
+ get bytes() { return bytes; },
97
+ };
98
+ }
99
+ export function sha256Parts(parts) {
100
+ const hash = createSHA256();
101
+ for (const part of parts) hash.update(part);
102
+ return hash.digest();
47
103
  }
104
+ export function sha256(text) { return createSHA256().update(text).digest(); }