@microsoft/sarif 0.0.1 → 5.3.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 +76 -7
- package/dist/aiRuleId.d.ts +27 -0
- package/dist/aiRuleId.js +60 -0
- package/dist/canonicalize.d.ts +12 -0
- package/dist/canonicalize.js +141 -0
- package/dist/index.d.ts +12 -5
- package/dist/index.js +12 -5
- package/dist/insertOptionalData.d.ts +39 -0
- package/dist/insertOptionalData.js +173 -0
- package/dist/io.d.ts +20 -0
- package/dist/io.js +77 -0
- package/dist/regions.d.ts +51 -0
- package/dist/regions.js +281 -0
- package/dist/rollingHash.d.ts +7 -0
- package/dist/rollingHash.js +111 -0
- package/dist/sarif.d.ts +227 -0
- package/dist/sarif.js +3 -0
- package/dist/serialization-model.json +949 -0
- package/dist/skill.d.ts +12 -0
- package/dist/skill.js +81 -0
- package/dist/vcp.d.ts +34 -0
- package/dist/vcp.js +227 -0
- package/package.json +4 -3
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
|
+
}
|
package/dist/regions.js
ADDED
|
@@ -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
|
+
}
|