@microsoft/sarif 0.0.1 → 5.2.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/dist/io.js ADDED
@@ -0,0 +1,77 @@
1
+ // Copyright (c) Microsoft. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+ /**
4
+ * SARIF serialization helpers + atomic file writer.
5
+ *
6
+ * Ported from src/Sarif/Emit/AtomicSarifWriter.cs and the Newtonsoft-parity
7
+ * serialization conventions used throughout the .NET SDK
8
+ * (NullValueHandling.Ignore, two-space indent).
9
+ */
10
+ import { promises as fs, existsSync, mkdirSync } from 'node:fs';
11
+ import { dirname, basename, resolve as resolvePath, join as joinPath } from 'node:path';
12
+ import { randomUUID } from 'node:crypto';
13
+ import { EOL } from 'node:os';
14
+ import { emitCanonicalJson } from './canonicalize.js';
15
+ /**
16
+ * Atomically writes a file by staging to a sibling temp file in the same
17
+ * directory and renaming over the destination. Mirrors AtomicSarifWriter.Write.
18
+ */
19
+ export async function atomicWrite(destinationPath, content) {
20
+ if (!destinationPath)
21
+ throw new Error('Destination path must be supplied.');
22
+ const full = resolvePath(destinationPath);
23
+ const dir = dirname(full);
24
+ if (!existsSync(dir))
25
+ mkdirSync(dir, { recursive: true });
26
+ const stagingPath = joinPath(dir, `${basename(full)}.${randomUUID().replace(/-/g, '')}.tmp`);
27
+ try {
28
+ const handle = await fs.open(stagingPath, 'wx');
29
+ try {
30
+ await handle.writeFile(content, 'utf8');
31
+ await handle.sync().catch(() => { });
32
+ }
33
+ finally {
34
+ await handle.close();
35
+ }
36
+ await fs.rename(stagingPath, full);
37
+ }
38
+ catch (err) {
39
+ await fs.unlink(stagingPath).catch(() => { });
40
+ throw err;
41
+ }
42
+ }
43
+ /**
44
+ * Recursively strips keys whose value is `null` or `undefined`, matching
45
+ * Newtonsoft `NullValueHandling.Ignore`. Arrays keep null entries (Newtonsoft
46
+ * does too); only object properties are pruned.
47
+ */
48
+ export function stripNulls(value) {
49
+ if (Array.isArray(value)) {
50
+ return value.map((v) => stripNulls(v));
51
+ }
52
+ if (value !== null && typeof value === 'object') {
53
+ const out = {};
54
+ for (const [k, v] of Object.entries(value)) {
55
+ if (v === null || v === undefined)
56
+ continue;
57
+ out[k] = stripNulls(v);
58
+ }
59
+ return out;
60
+ }
61
+ return value;
62
+ }
63
+ /**
64
+ * Serializes a SarifLog to match the .NET multitool byte-for-byte: model-order
65
+ * keys, two-space indent, null-stripping, verbatim-compact property-bag values
66
+ * (a Newtonsoft trait — bag values are stored as pre-serialized JToken text),
67
+ * `Environment.NewLine` line endings, and no trailing newline. See
68
+ * canonicalize.ts for the ordering/emit details.
69
+ */
70
+ export function serializeSarifLog(log, prettyPrint = true) {
71
+ const json = emitCanonicalJson(stripNulls(log), prettyPrint);
72
+ // Newtonsoft's indented writer uses Environment.NewLine; mirror it so a
73
+ // Windows-generated fixture is CRLF and a Linux one is LF, matching .NET on
74
+ // the same host. Only structural newlines are affected — JSON escapes any
75
+ // newline inside a string value as \n, so it never reaches this split.
76
+ return prettyPrint && EOL !== '\n' ? json.split('\n').join(EOL) : json;
77
+ }
@@ -0,0 +1,51 @@
1
+ import type { Region } from './sarif.js';
2
+ /**
3
+ * Index of newline start locations so a char offset can cheaply turn into a
4
+ * 1-based line/column and vice versa. Mirrors NewLineIndex.cs.
5
+ */
6
+ export declare class NewLineIndex {
7
+ readonly text: string;
8
+ private readonly lineOffsetStarts;
9
+ constructor(text: string);
10
+ get maximumLineNumber(): number;
11
+ /** Char offset at which the given 1-based line begins. */
12
+ startOffsetForLine(lineNumber: number): number;
13
+ /** 1-based {line, column} for the given char offset. */
14
+ offsetInfo(offset: number): {
15
+ lineNumber: number;
16
+ columnNumber: number;
17
+ };
18
+ }
19
+ export declare class FileRegionsCache {
20
+ private readonly textCache;
21
+ private readonly indexCache;
22
+ private readonly hashCache;
23
+ private readonly rollingHashCache;
24
+ private localPathFor;
25
+ /** Returns file text or null if unreadable. */
26
+ getText(absoluteUri: string): string | null;
27
+ getNewLineIndex(absoluteUri: string): NewLineIndex | null;
28
+ /** sha-256 over the raw bytes of the file. */
29
+ getSha256(absoluteUri: string): string | undefined;
30
+ /**
31
+ * CodeQL-style per-line rolling-hash fingerprints for the file, keyed by
32
+ * 1-based line number, or null if the file is unreadable. Mirrors
33
+ * InsertOptionalDataVisitor.GetRollingHashes.
34
+ */
35
+ getRollingHashes(absoluteUri: string): Map<number, string> | null;
36
+ /**
37
+ * Returns a copy of `inputRegion` with all text-related properties
38
+ * populated (line/col ↔ charOffset/charLength) and optionally a snippet.
39
+ * Mirrors FileRegionsCache.PopulateTextRegionProperties.
40
+ *
41
+ * `overwriteExistingData=false` (the default) throws on author/source
42
+ * coordinate divergence; `true` replaces the authored value.
43
+ */
44
+ populateTextRegionProperties(inputRegion: Region, absoluteUri: string, populateSnippet: boolean, overwriteExistingData?: boolean): Region;
45
+ /**
46
+ * Builds a context region that spans roughly one line above and one line
47
+ * below the input region (or a ±128-char window if that overflows 512
48
+ * chars). Mirrors FileRegionsCache.ConstructMultilineContextSnippet.
49
+ */
50
+ constructMultilineContextSnippet(inputRegion: Region, absoluteUri: string): Region | undefined;
51
+ }
@@ -0,0 +1,281 @@
1
+ // Copyright (c) Microsoft. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+ /**
4
+ * NewLineIndex + FileRegionsCache: line/column ↔ char-offset resolution and
5
+ * snippet / context-region extraction.
6
+ *
7
+ * Ported from:
8
+ * src/Sarif/NewLineIndex.cs
9
+ * src/Sarif/FileRegionsCache.cs (text-region subset)
10
+ */
11
+ import { readFileSync } from 'node:fs';
12
+ import { createHash } from 'node:crypto';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { computeRollingHashes } from './rollingHash.js';
15
+ const NEWLINE_CHARS = new Set(['\n', '\r']);
16
+ const BIG_SNIPPET_LENGTH = 512;
17
+ const SMALL_SNIPPET_LENGTH = 128;
18
+ // ---------------------------------------------------------------------------
19
+ // NewLineIndex
20
+ // ---------------------------------------------------------------------------
21
+ /**
22
+ * Index of newline start locations so a char offset can cheaply turn into a
23
+ * 1-based line/column and vice versa. Mirrors NewLineIndex.cs.
24
+ */
25
+ export class NewLineIndex {
26
+ text;
27
+ // lineOffsetStarts[n] is the char offset at which (1-based) line n+1 begins.
28
+ lineOffsetStarts;
29
+ constructor(text) {
30
+ this.text = text;
31
+ const starts = [0];
32
+ for (let i = 0; i < text.length; i++) {
33
+ const c = text[i];
34
+ // Detect \r, \n, but NOT the \r in \r\n (the following \n handles it).
35
+ if (NEWLINE_CHARS.has(c)) {
36
+ if (c !== '\r' || i + 1 >= text.length || text[i + 1] !== '\n') {
37
+ starts.push(i + 1);
38
+ }
39
+ }
40
+ }
41
+ this.lineOffsetStarts = starts;
42
+ }
43
+ get maximumLineNumber() {
44
+ return this.lineOffsetStarts.length;
45
+ }
46
+ /** Char offset at which the given 1-based line begins. */
47
+ startOffsetForLine(lineNumber) {
48
+ if (lineNumber <= 0 || lineNumber > this.maximumLineNumber + 1) {
49
+ throw new RangeError(`Line number ${lineNumber} is out of range.`);
50
+ }
51
+ if (lineNumber === this.maximumLineNumber + 1)
52
+ return this.text.length;
53
+ return this.lineOffsetStarts[lineNumber - 1];
54
+ }
55
+ /** 1-based {line, column} for the given char offset. */
56
+ offsetInfo(offset) {
57
+ if (offset < 0)
58
+ throw new RangeError('Offset cannot be negative.');
59
+ // Binary-search for the greatest start ≤ offset.
60
+ let lo = 0, hi = this.lineOffsetStarts.length - 1, idx = 0;
61
+ while (lo <= hi) {
62
+ const mid = (lo + hi) >> 1;
63
+ const v = this.lineOffsetStarts[mid];
64
+ if (v === offset) {
65
+ idx = mid;
66
+ break;
67
+ }
68
+ if (v < offset) {
69
+ idx = mid;
70
+ lo = mid + 1;
71
+ }
72
+ else {
73
+ hi = mid - 1;
74
+ }
75
+ }
76
+ const startOffset = this.lineOffsetStarts[idx];
77
+ return { lineNumber: idx + 1, columnNumber: offset - startOffset + 1 };
78
+ }
79
+ }
80
+ // ---------------------------------------------------------------------------
81
+ // FileRegionsCache (text-region subset)
82
+ // ---------------------------------------------------------------------------
83
+ export class FileRegionsCache {
84
+ textCache = new Map();
85
+ indexCache = new Map();
86
+ hashCache = new Map();
87
+ rollingHashCache = new Map();
88
+ localPathFor(absoluteUri) {
89
+ try {
90
+ const u = new URL(absoluteUri);
91
+ if (u.protocol === 'file:')
92
+ return fileURLToPath(u);
93
+ }
94
+ catch {
95
+ /* not a URL */
96
+ }
97
+ return undefined;
98
+ }
99
+ /** Returns file text or null if unreadable. */
100
+ getText(absoluteUri) {
101
+ if (this.textCache.has(absoluteUri))
102
+ return this.textCache.get(absoluteUri);
103
+ const path = this.localPathFor(absoluteUri);
104
+ let text = null;
105
+ if (path) {
106
+ try {
107
+ text = readFileSync(path, 'utf8');
108
+ }
109
+ catch {
110
+ text = null;
111
+ }
112
+ }
113
+ this.textCache.set(absoluteUri, text);
114
+ return text;
115
+ }
116
+ getNewLineIndex(absoluteUri) {
117
+ if (this.indexCache.has(absoluteUri))
118
+ return this.indexCache.get(absoluteUri);
119
+ const text = this.getText(absoluteUri);
120
+ const idx = text !== null ? new NewLineIndex(text) : null;
121
+ this.indexCache.set(absoluteUri, idx);
122
+ return idx;
123
+ }
124
+ /** sha-256 over the raw bytes of the file. */
125
+ getSha256(absoluteUri) {
126
+ if (this.hashCache.has(absoluteUri))
127
+ return this.hashCache.get(absoluteUri);
128
+ const path = this.localPathFor(absoluteUri);
129
+ if (!path)
130
+ return undefined;
131
+ let buf;
132
+ try {
133
+ buf = readFileSync(path);
134
+ }
135
+ catch {
136
+ return undefined;
137
+ }
138
+ // .NET HashUtilities emits uppercase hex; match it for byte parity.
139
+ const hash = createHash('sha256').update(buf).digest('hex').toUpperCase();
140
+ this.hashCache.set(absoluteUri, hash);
141
+ return hash;
142
+ }
143
+ /**
144
+ * CodeQL-style per-line rolling-hash fingerprints for the file, keyed by
145
+ * 1-based line number, or null if the file is unreadable. Mirrors
146
+ * InsertOptionalDataVisitor.GetRollingHashes.
147
+ */
148
+ getRollingHashes(absoluteUri) {
149
+ if (this.rollingHashCache.has(absoluteUri))
150
+ return this.rollingHashCache.get(absoluteUri);
151
+ const text = this.getText(absoluteUri);
152
+ const hashes = text !== null ? computeRollingHashes(text) : null;
153
+ this.rollingHashCache.set(absoluteUri, hashes);
154
+ return hashes;
155
+ }
156
+ /**
157
+ * Returns a copy of `inputRegion` with all text-related properties
158
+ * populated (line/col ↔ charOffset/charLength) and optionally a snippet.
159
+ * Mirrors FileRegionsCache.PopulateTextRegionProperties.
160
+ *
161
+ * `overwriteExistingData=false` (the default) throws on author/source
162
+ * coordinate divergence; `true` replaces the authored value.
163
+ */
164
+ populateTextRegionProperties(inputRegion, absoluteUri, populateSnippet, overwriteExistingData = false) {
165
+ if (!inputRegion || isBinaryRegion(inputRegion))
166
+ return inputRegion;
167
+ const idx = this.getNewLineIndex(absoluteUri);
168
+ if (!idx)
169
+ return inputRegion;
170
+ const fileText = idx.text;
171
+ const region = { ...inputRegion };
172
+ if (!region.startLine || region.startLine === 0) {
173
+ populateFromCharOffsetAndLength(idx, region, overwriteExistingData);
174
+ }
175
+ else {
176
+ populateFromStartAndEndProperties(idx, region, fileText, overwriteExistingData);
177
+ }
178
+ if (populateSnippet &&
179
+ (region.charOffset ?? -1) >= 0 &&
180
+ (region.charLength ?? -1) >= 0 &&
181
+ (region.charOffset ?? 0) + (region.charLength ?? 0) <= fileText.length) {
182
+ region.snippet ??= {};
183
+ const snippetText = fileText.substring(region.charOffset, region.charOffset + region.charLength);
184
+ if (region.snippet.text === undefined) {
185
+ region.snippet.text = snippetText;
186
+ }
187
+ }
188
+ return region;
189
+ }
190
+ /**
191
+ * Builds a context region that spans roughly one line above and one line
192
+ * below the input region (or a ±128-char window if that overflows 512
193
+ * chars). Mirrors FileRegionsCache.ConstructMultilineContextSnippet.
194
+ */
195
+ constructMultilineContextSnippet(inputRegion, absoluteUri) {
196
+ if (!inputRegion || isBinaryRegion(inputRegion))
197
+ return undefined;
198
+ const idx = this.getNewLineIndex(absoluteUri);
199
+ if (!idx)
200
+ return undefined;
201
+ const fileText = idx.text;
202
+ const original = this.populateTextRegionProperties(inputRegion, absoluteUri, true);
203
+ if ((original.charLength ?? 0) >= BIG_SNIPPET_LENGTH) {
204
+ return { ...original };
205
+ }
206
+ const maxLine = idx.maximumLineNumber;
207
+ const startLine = (inputRegion.startLine ?? 1) === 1 ? 1 : (inputRegion.startLine ?? 1) - 1;
208
+ const endLineIn = inputRegion.endLine ?? inputRegion.startLine ?? 1;
209
+ const endLine = endLineIn === maxLine ? maxLine : endLineIn + 1;
210
+ let context = this.populateTextRegionProperties({ startLine, endLine }, absoluteUri, true);
211
+ if ((original.charLength ?? 0) <= (context.charLength ?? 0) &&
212
+ (context.charLength ?? 0) <= BIG_SNIPPET_LENGTH) {
213
+ return context;
214
+ }
215
+ const charOffset = Math.max(0, (original.charOffset ?? 0) - SMALL_SNIPPET_LENGTH);
216
+ const charLength = Math.min(BIG_SNIPPET_LENGTH, fileText.length - charOffset);
217
+ context = this.populateTextRegionProperties({ charOffset, charLength }, absoluteUri, true);
218
+ return context;
219
+ }
220
+ }
221
+ // ---------------------------------------------------------------------------
222
+ // Region coordinate population (mirrors private helpers in FileRegionsCache)
223
+ // ---------------------------------------------------------------------------
224
+ function isBinaryRegion(r) {
225
+ return r.byteOffset !== undefined && r.byteOffset >= 0;
226
+ }
227
+ function reconcile(overwrite, propertyName, computed, authored) {
228
+ if (authored === computed)
229
+ return authored;
230
+ if (overwrite)
231
+ return computed;
232
+ throw new Error(`The input region specifies '${propertyName}' = ${authored}, but the value computed from the source file is ${computed}. Authored region coordinates must exactly match the source text; omit a coordinate to have the SDK compute it, or set overwriteExistingData to recompute (overwrite) it.`);
233
+ }
234
+ function populateFromCharOffsetAndLength(idx, region, overwrite) {
235
+ const charOffset = region.charOffset ?? 0;
236
+ const charLength = region.charLength ?? 0;
237
+ const start = idx.offsetInfo(charOffset);
238
+ const end = idx.offsetInfo(charOffset + charLength);
239
+ if (!region.startLine)
240
+ region.startLine = start.lineNumber;
241
+ if (!region.startColumn)
242
+ region.startColumn = start.columnNumber;
243
+ if (!region.endLine)
244
+ region.endLine = end.lineNumber;
245
+ if (!region.endColumn)
246
+ region.endColumn = end.columnNumber;
247
+ region.startLine = reconcile(overwrite, 'startLine', start.lineNumber, region.startLine);
248
+ region.startColumn = reconcile(overwrite, 'startColumn', start.columnNumber, region.startColumn);
249
+ region.endLine = reconcile(overwrite, 'endLine', end.lineNumber, region.endLine);
250
+ region.endColumn = reconcile(overwrite, 'endColumn', end.columnNumber, region.endColumn);
251
+ }
252
+ function populateFromStartAndEndProperties(idx, region, fileText, overwrite) {
253
+ // Order is significant; each step relies on coordinates populated earlier.
254
+ region.endLine = region.endLine && region.endLine !== 0 ? region.endLine : region.startLine;
255
+ region.startColumn = region.startColumn && region.startColumn !== 0 ? region.startColumn : 1;
256
+ if (!region.endColumn || region.endColumn === 0) {
257
+ // No explicit end column: scan to the line terminator.
258
+ const lineStart = idx.startOffsetForLine(region.endLine);
259
+ let p = lineStart;
260
+ while (p < fileText.length && !NEWLINE_CHARS.has(fileText[p]))
261
+ p++;
262
+ region.endColumn = p - lineStart + 1;
263
+ }
264
+ // charOffset
265
+ const startLineOffset = idx.startOffsetForLine(region.startLine);
266
+ const computedOffset = startLineOffset + region.startColumn - 1;
267
+ if (region.charOffset === undefined || region.charOffset === 0 || region.charOffset === -1) {
268
+ region.charOffset = computedOffset;
269
+ }
270
+ region.charOffset = reconcile(overwrite, 'charOffset', computedOffset, region.charOffset);
271
+ // charLength
272
+ const endLineOffset = idx.startOffsetForLine(region.endLine);
273
+ const computedLength = endLineOffset - region.charOffset + region.endColumn - 1;
274
+ if (region.charLength === undefined || region.charLength === 0) {
275
+ region.charLength = computedLength;
276
+ }
277
+ region.charLength = reconcile(overwrite, 'charLength', computedLength, region.charLength);
278
+ if (region.charOffset + region.charLength > fileText.length && !overwrite) {
279
+ throw new Error(`The input region's character span (charOffset ${region.charOffset} + charLength ${region.charLength} = ${region.charOffset + region.charLength}) extends beyond the source file length (${fileText.length}). Authored region coordinates must lie within the source text; omit them to have the SDK compute the span, or set overwriteExistingData to recompute it.`);
280
+ }
281
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Computes the rolling-hash fingerprint for every line of `fileText`, keyed by
3
+ * 1-based line number, with values of the form `"<hex>:<ordinal>"` where the
4
+ * ordinal disambiguates lines whose hash collides (identical normalized
5
+ * content). Mirrors HashUtilities.RollingHash.
6
+ */
7
+ export declare function computeRollingHashes(fileText: string): Map<number, string>;
@@ -0,0 +1,111 @@
1
+ // Copyright (c) Microsoft. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+ /**
4
+ * CodeQL-style rolling-hash partial fingerprints (`primaryLocationLineHash`).
5
+ *
6
+ * The normative source for this algorithm is github/codeql-action's
7
+ * fingerprints.ts (https://github.com/github/codeql-action, src/fingerprints.ts);
8
+ * src/Sarif/HashUtilities.cs is itself a port of it, and src/Sarif/Numeric/Long.cs
9
+ * exists only to re-emulate JS 64-bit `Long` arithmetic in the CLR. This module
10
+ * returns to that source: it reproduces the same per-line `"<hex>:<count>"`
11
+ * fingerprints byte-for-byte using native BigInt mod-2^64 arithmetic, so no
12
+ * `Long` emulation (or npm `long` dependency) is needed.
13
+ *
14
+ * GitHub's raw code-scanning SARIF upload API does NOT backfill
15
+ * partialFingerprints, so a GitHub-hosted producer must stamp this itself to
16
+ * avoid duplicate alerts across scans. See InsertOptionalDataVisitor.cs.
17
+ */
18
+ const TAB = 0x09;
19
+ const SPACE = 0x20;
20
+ const LF = 0x0a;
21
+ const CR = 0x0d;
22
+ const EOF = 65535;
23
+ const BLOCK_SIZE = 100;
24
+ // 64-bit two's-complement ring. BigInt `& MASK` reduces any (incl. negative)
25
+ // intermediate to its low 64 bits, exactly matching the CLR `Long` wraparound
26
+ // and `ToUnsigned()` the C# performs before `ToString(16)`.
27
+ const MASK = (1n << 64n) - 1n;
28
+ const MOD = 37n;
29
+ function computeFirstMod() {
30
+ let firstMod = 1n;
31
+ for (let i = 0; i < BLOCK_SIZE; i++) {
32
+ firstMod = (firstMod * MOD) & MASK;
33
+ }
34
+ return firstMod;
35
+ }
36
+ /**
37
+ * Computes the rolling-hash fingerprint for every line of `fileText`, keyed by
38
+ * 1-based line number, with values of the form `"<hex>:<ordinal>"` where the
39
+ * ordinal disambiguates lines whose hash collides (identical normalized
40
+ * content). Mirrors HashUtilities.RollingHash.
41
+ */
42
+ export function computeRollingHashes(fileText) {
43
+ const rollingHashes = new Map();
44
+ if (fileText == null)
45
+ return rollingHashes;
46
+ // A rolling view into the input.
47
+ const window = new Array(BLOCK_SIZE).fill(0);
48
+ const lineNumbers = new Array(BLOCK_SIZE).fill(-1);
49
+ let hashRaw = 0n;
50
+ const firstMod = computeFirstMod();
51
+ let index = 0;
52
+ let lineNumber = 0;
53
+ let lineStart = true;
54
+ let prevCR = false;
55
+ const hashCounts = new Map();
56
+ const outputHash = () => {
57
+ const hashValue = (hashRaw & MASK).toString(16);
58
+ const count = (hashCounts.get(hashValue) ?? 0) + 1;
59
+ hashCounts.set(hashValue, count);
60
+ rollingHashes.set(lineNumbers[index], `${hashValue}:${count}`);
61
+ lineNumbers[index] = -1;
62
+ };
63
+ const updateHash = (current) => {
64
+ const begin = window[index];
65
+ window[index] = current;
66
+ hashRaw = (MOD * hashRaw + BigInt(current) - firstMod * BigInt(begin)) & MASK;
67
+ index = (index + 1) % BLOCK_SIZE;
68
+ };
69
+ const processCharacter = (current) => {
70
+ // Skip tabs, spaces, and line feeds that come directly after a carriage return.
71
+ if (current === SPACE || current === TAB || (prevCR && current === LF)) {
72
+ prevCR = false;
73
+ return;
74
+ }
75
+ // Replace CR with LF. (\u2028 / \u2029 are intentionally not handled,
76
+ // matching the reference implementation.)
77
+ if (current === CR) {
78
+ current = LF;
79
+ prevCR = true;
80
+ }
81
+ else {
82
+ prevCR = false;
83
+ }
84
+ if (lineNumbers[index] !== -1) {
85
+ outputHash();
86
+ }
87
+ if (lineStart) {
88
+ lineStart = false;
89
+ lineNumber++;
90
+ lineNumbers[index] = lineNumber;
91
+ }
92
+ if (current === LF) {
93
+ lineStart = true;
94
+ }
95
+ updateHash(current);
96
+ };
97
+ // `charCodeAt` yields UTF-16 code units, matching the C# `char` iteration the
98
+ // reference algorithm is defined over.
99
+ for (let i = 0; i < fileText.length; i++) {
100
+ processCharacter(fileText.charCodeAt(i));
101
+ }
102
+ processCharacter(EOF);
103
+ // Flush the remaining lines.
104
+ for (let i = 0; i < BLOCK_SIZE; i++) {
105
+ if (lineNumbers[index] !== -1) {
106
+ outputHash();
107
+ }
108
+ updateHash(0);
109
+ }
110
+ return rollingHashes;
111
+ }