mirror of
https://github.com/microsoft/vscode.git
synced 2026-09-10 03:44:24 +01:00
fix: validate file stat size/mtime before SQLite bind in external ingest index (fixes #321794) (#321799)
* fix: reject non-numeric file stats before SQLite bind in external ingest index Virtual/custom file system providers can return a FileStat whose `size` or `mtime` is not a number, violating the declared `number` type of safeStat(). These values were passed straight into a node:sqlite parameter bind, which rejects any non-number value and throws the unhandled error "Provided value cannot be bound to SQLite parameter 2". Validate the numeric contract at the file-system boundary in safeStat() and treat a stat with a non-numeric size or mtime as unusable (return undefined), matching the function's existing behaviour for directories and stat failures. Fixes #321794 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: reject non-finite external ingest file stats Use finite-number validation at the filesystem boundary so NaN and infinity cannot reach SQLite. Cover malformed provider metadata while ensuring later valid files are still indexed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: vs-code-engineering[bot] <122617954+vs-code-engineering[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Dmitriy Vasyura <dmitriv@microsoft.com>
This commit is contained in:
co-authored by
Copilot
Dmitriy Vasyura
parent
dbcab2ff18
commit
6a99cd4fe5
+4
-2
@@ -1042,6 +1042,10 @@ export class ExternalIngestIndex extends Disposable {
|
||||
if (stat.type !== 1) { // FileType.File = 1
|
||||
return undefined;
|
||||
}
|
||||
// File system providers may violate FileStat's numeric contract at runtime.
|
||||
if (!Number.isFinite(stat.size) || !Number.isFinite(stat.mtime)) {
|
||||
return undefined;
|
||||
}
|
||||
return { size: stat.size, mtime: stat.mtime };
|
||||
} catch {
|
||||
return undefined;
|
||||
@@ -1098,5 +1102,3 @@ export class ExternalIngestIndex extends Disposable {
|
||||
return { fileCount: files.length, files };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+45
-1
@@ -84,6 +84,10 @@ interface MockFileEntry {
|
||||
readonly content: Uint8Array;
|
||||
readonly size: number;
|
||||
readonly mtime: number;
|
||||
readonly statOverride?: {
|
||||
readonly field: 'size' | 'mtime';
|
||||
readonly value?: number | string;
|
||||
};
|
||||
}
|
||||
|
||||
function createFileFromString(content: string, mtime = Date.now()): MockFileEntry {
|
||||
@@ -126,13 +130,18 @@ class MockFileSystem extends mock<IFileSystemService & ISearchService>() impleme
|
||||
if (!entry) {
|
||||
throw new Error(`File not found: ${uri.toString()}`);
|
||||
}
|
||||
return {
|
||||
const stat = {
|
||||
type: FileType.File,
|
||||
ctime: 0,
|
||||
mtime: entry.mtime,
|
||||
size: entry.size,
|
||||
permissions: undefined,
|
||||
};
|
||||
if (entry.statOverride) {
|
||||
// Simulate a provider violating FileStat's runtime contract.
|
||||
Object.defineProperty(stat, entry.statOverride.field, { value: entry.statOverride.value });
|
||||
}
|
||||
return stat;
|
||||
}
|
||||
|
||||
override async readFile(uri: URI) {
|
||||
@@ -359,6 +368,41 @@ suite('ExternalIngestIndex', () => {
|
||||
assert.strictEqual(await index.shouldTrackFile(file2, CancellationToken.None), true);
|
||||
});
|
||||
|
||||
test('initialize skips malformed file stats and continues indexing', async () => {
|
||||
const workspaceRoot = URI.file('/workspace');
|
||||
const malformedStats: readonly {
|
||||
readonly name: string;
|
||||
readonly field: 'size' | 'mtime';
|
||||
readonly value?: number | string;
|
||||
}[] = [
|
||||
{ name: 'missing-size.ts', field: 'size' },
|
||||
{ name: 'string-mtime.ts', field: 'mtime', value: 'invalid' },
|
||||
{ name: 'nan-size.ts', field: 'size', value: Number.NaN },
|
||||
{ name: 'infinite-mtime.ts', field: 'mtime', value: Number.POSITIVE_INFINITY },
|
||||
];
|
||||
const files = new ResourceMap<MockFileEntry>();
|
||||
for (const { name, field, value } of malformedStats) {
|
||||
files.set(URI.joinPath(workspaceRoot, name), {
|
||||
...createFileFromString('const invalid = true;'),
|
||||
statOverride: { field, value },
|
||||
});
|
||||
}
|
||||
const validFile = URI.joinPath(workspaceRoot, 'valid.ts');
|
||||
files.set(validFile, createFileFromString('const valid = true;'));
|
||||
|
||||
const { mockClient, index } = setupTestContext(workspaceRoot, files);
|
||||
await index.initialize();
|
||||
const ingestResult = await index.doIngest(testTelemetryInfo, emptyProgressCb, CancellationToken.None);
|
||||
|
||||
assert.deepStrictEqual({
|
||||
ingestSucceeded: ingestResult.isOk(),
|
||||
ingestedFiles: mockClient.ingestedFiles.map(file => file.uri.toString()),
|
||||
}, {
|
||||
ingestSucceeded: true,
|
||||
ingestedFiles: [validFile.toString()],
|
||||
});
|
||||
});
|
||||
|
||||
test('files that fail canIngestPathAndSize are tracked but not ingested', async () => {
|
||||
const workspaceRoot = URI.file('/workspace');
|
||||
const file1 = URI.joinPath(workspaceRoot, 'small.ts');
|
||||
|
||||
Reference in New Issue
Block a user