mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-28 12:35:13 +02:00
169 lines
7.4 KiB
JavaScript
169 lines
7.4 KiB
JavaScript
'use strict';
|
|
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const test = require('node:test');
|
|
const { createSourceReader } = require('../../scripts/lib/context-profile-support');
|
|
const { withFixture } = require('./helpers/context-fixture');
|
|
|
|
const DIRECTORY_LIMIT = 10000;
|
|
const TRAVERSAL_LIMIT = 20000;
|
|
|
|
function mockEnumeration(context, entriesFor) {
|
|
const counts = { opens: 0, reads: 0, closes: 0, wholeDirectoryReads: 0 };
|
|
// Resolve Node 20's lazy fs.opendirSync export before readdirSync is mocked.
|
|
const originalOpenDirectory = fs.opendirSync;
|
|
context.mock.method(fs, 'readdirSync', filename => {
|
|
counts.wholeDirectoryReads++;
|
|
return entriesFor(filename);
|
|
});
|
|
context.mock.method(fs, 'opendirSync', (filename, options) => {
|
|
assert.equal(options.bufferSize, 32);
|
|
counts.opens++;
|
|
const entries = entriesFor(filename);
|
|
let index = 0;
|
|
return {
|
|
readSync() { counts.reads++; return index < entries.length ? { name: entries[index++] } : null; },
|
|
closeSync() { counts.closes++; },
|
|
};
|
|
});
|
|
assert.equal(typeof originalOpenDirectory, 'function');
|
|
return counts;
|
|
}
|
|
|
|
function changedIdentity(stats) {
|
|
const changed = Object.assign(Object.create(Object.getPrototypeOf(stats)), stats);
|
|
// Windows file IDs can exceed Number's integer precision, so ino + 1 may
|
|
// equal ino. Use an exactly representable identity that always differs.
|
|
const zero = typeof stats.ino === 'bigint' ? 0n : 0;
|
|
const one = typeof stats.ino === 'bigint' ? 1n : 1;
|
|
changed.ino = stats.ino === zero ? one : zero;
|
|
return changed;
|
|
}
|
|
|
|
function samePath(left, right) {
|
|
const normalize = value => path.resolve(value).toLowerCase();
|
|
return normalize(left) === normalize(right);
|
|
}
|
|
|
|
test('wide directories stop after one bounded lookahead without allocating a whole listing', context => withFixture(root => {
|
|
const reader = createSourceReader(root);
|
|
const counts = mockEnumeration(context, () => Array.from({ length: DIRECTORY_LIMIT + 100 }, (_, index) => `entry-${index}`));
|
|
try {
|
|
assert.throws(() => reader.list('skills'), /directory.*limit/i);
|
|
assert.equal(counts.wholeDirectoryReads, 0);
|
|
assert.equal(counts.reads, DIRECTORY_LIMIT + 1);
|
|
assert.equal(counts.closes, 1);
|
|
} finally { context.mock.restoreAll(); }
|
|
}));
|
|
|
|
test('exact per-directory limit is accepted and sorted only after bounded enumeration', context => withFixture(root => {
|
|
const reader = createSourceReader(root);
|
|
const names = Array.from({ length: DIRECTORY_LIMIT }, (_, index) => `entry-${String(index).padStart(5, '0')}`);
|
|
const counts = mockEnumeration(context, () => [...names].reverse());
|
|
try {
|
|
assert.deepEqual(reader.list('skills'), names);
|
|
assert.equal(counts.wholeDirectoryReads, 0);
|
|
assert.equal(counts.reads, DIRECTORY_LIMIT + 1);
|
|
assert.equal(counts.closes, 1);
|
|
} finally { context.mock.restoreAll(); }
|
|
}));
|
|
|
|
test('directory-only breadth consumes the shared traversal budget even when no files exist', context => withFixture(root => {
|
|
const reader = createSourceReader(root);
|
|
const base = path.join(fs.realpathSync(root), 'skills/feature');
|
|
const directoryStats = fs.lstatSync(base);
|
|
const originalStat = fs.lstatSync;
|
|
const names = Array.from({ length: DIRECTORY_LIMIT }, (_, index) => `dir-${index}`);
|
|
const counts = mockEnumeration(context, filename => filename === base ? names : []);
|
|
context.mock.method(fs, 'lstatSync', (filename, ...args) => (
|
|
filename.startsWith(`${base}${path.sep}dir-`) ? directoryStats : originalStat(filename, ...args)
|
|
));
|
|
context.mock.method(fs, 'openSync', () => { throw new Error('Directory-only traversal must not open file bytes'); });
|
|
try {
|
|
assert.throws(() => reader.walk('skills/feature'), /traversal.*limit/i);
|
|
assert.equal(counts.wholeDirectoryReads, 0);
|
|
assert.equal(counts.opens + names.length, TRAVERSAL_LIMIT);
|
|
assert.equal(counts.closes, counts.opens);
|
|
} finally { context.mock.restoreAll(); }
|
|
}));
|
|
|
|
test('excluded cache names consume enumeration limits before filtering', context => withFixture(root => {
|
|
const reader = createSourceReader(root);
|
|
const counts = mockEnumeration(context, () => Array.from({ length: DIRECTORY_LIMIT + 1 }, (_, index) => `cache-${index}.pyc`));
|
|
try {
|
|
assert.throws(() => reader.walk('skills/feature'), /directory.*limit/i);
|
|
assert.equal(counts.reads, DIRECTORY_LIMIT + 1);
|
|
assert.equal(counts.closes, 1);
|
|
} finally { context.mock.restoreAll(); }
|
|
}));
|
|
|
|
test('enumeration errors close the directory handle', context => withFixture(root => {
|
|
const reader = createSourceReader(root);
|
|
const directory = path.join(fs.realpathSync(root), 'skills');
|
|
const originalOpen = fs.opendirSync;
|
|
const originalRead = fs.readdirSync;
|
|
let closes = 0;
|
|
context.mock.method(fs, 'opendirSync', (filename, options) => samePath(filename, directory) ? ({
|
|
readSync() { throw new Error('TEST_DIRECTORY_READ_FAILURE'); },
|
|
closeSync() { closes++; },
|
|
}) : originalOpen(filename, options));
|
|
context.mock.method(fs, 'readdirSync', (filename, options) => {
|
|
if (!samePath(filename, directory)) return originalRead(filename, options);
|
|
throw new Error('TEST_DIRECTORY_READ_FAILURE');
|
|
});
|
|
try {
|
|
assert.throws(() => reader.list('skills'), /TEST_DIRECTORY_READ_FAILURE/);
|
|
assert.equal(closes, 1);
|
|
} finally { context.mock.restoreAll(); }
|
|
}));
|
|
|
|
for (const inode of [undefined, 2 ** 60]) {
|
|
const identityLabel = inode === undefined ? 'host inode' : 'large Windows-style inode';
|
|
|
|
test(`directory identity changes during open close the handle before reading any entries (${identityLabel})`, context => withFixture(root => {
|
|
const reader = createSourceReader(root);
|
|
const directory = path.join(fs.realpathSync(root), 'skills');
|
|
const originalStat = fs.lstatSync;
|
|
let opened = false;
|
|
let reads = 0;
|
|
let closes = 0;
|
|
context.mock.method(fs, 'opendirSync', () => {
|
|
opened = true;
|
|
return { readSync() { reads++; return null; }, closeSync() { closes++; } };
|
|
});
|
|
context.mock.method(fs, 'lstatSync', (filename, ...args) => {
|
|
const stats = originalStat(filename, ...args);
|
|
if (samePath(filename, directory) && inode !== undefined) stats.ino = inode;
|
|
return opened && samePath(filename, directory) ? changedIdentity(stats) : stats;
|
|
});
|
|
try {
|
|
assert.throws(() => reader.list('skills'), /identity.*changed/i);
|
|
assert.equal(reads, 0);
|
|
assert.equal(closes, 1);
|
|
} finally { context.mock.restoreAll(); }
|
|
}));
|
|
|
|
test(`directory identity changes during enumeration reject the result and close the handle (${identityLabel})`, context => withFixture(root => {
|
|
const reader = createSourceReader(root);
|
|
const directory = path.join(fs.realpathSync(root), 'skills');
|
|
const originalStat = fs.lstatSync;
|
|
let enumerated = false;
|
|
let closes = 0;
|
|
context.mock.method(fs, 'opendirSync', () => ({
|
|
readSync() { enumerated = true; return null; },
|
|
closeSync() { closes++; },
|
|
}));
|
|
context.mock.method(fs, 'lstatSync', (filename, ...args) => {
|
|
const stats = originalStat(filename, ...args);
|
|
if (samePath(filename, directory) && inode !== undefined) stats.ino = inode;
|
|
return enumerated && samePath(filename, directory) ? changedIdentity(stats) : stats;
|
|
});
|
|
try {
|
|
assert.throws(() => reader.list('skills'), /identity.*changed/i);
|
|
assert.equal(closes, 1);
|
|
} finally { context.mock.restoreAll(); }
|
|
}));
|
|
}
|