fix(scan): don't abort indexing on a non-UTF-8 or unparseable .gitignore (#682) (#743)

A .gitignore transparently encrypted in place by corporate DLP / endpoint
software (UTF-16 header + ciphertext), or one containing a pattern the
`ignore` library can't compile to a regex (`\[` -> "Unterminated character
class"), crashed the entire sync/index. The throw is LAZY — it surfaces at
match time (`ig.ignores()`), not `.add()` — so the existing add-time
try/catch never caught it, and the error never named the offending file.

Read .gitignore defensively: skip a file that isn't valid UTF-8 text whole
(NUL byte or fatal UTF-8 decode), drop only the individual uncompilable
patterns from a text one (probe-compile, then per-line fallback), and warn
with the file path. Indexing continues either way. The watcher inherits the
fix via buildDefaultIgnore.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-06-08 21:36:44 -04:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 6e2a24d96a
commit 35b44e242c
3 changed files with 131 additions and 16 deletions
+49 -1
View File
@@ -9,7 +9,7 @@ import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { CodeGraph } from '../src';
import { extractFromSource, scanDirectory } from '../src/extraction';
import { extractFromSource, scanDirectory, buildDefaultIgnore } from '../src/extraction';
import { detectLanguage, isLanguageSupported, getSupportedLanguages, initGrammars, loadAllGrammars, isSourceFile } from '../src/extraction/grammars';
import { normalizePath } from '../src/utils';
@@ -5245,6 +5245,54 @@ describe('Nested non-submodule git repos', () => {
expect(files).toContain('sub_repo/src/real.ts');
expect(files).not.toContain('sub_repo/src/generated.ts');
});
// A .gitignore the `ignore` library can't compile to a regex must not abort
// the whole scan — the bad pattern is dropped, valid ones still apply (#682).
it('does not crash on a .gitignore with an uncompilable pattern (#682)', () => {
fs.mkdirSync(path.join(tempDir, 'src'), { recursive: true });
fs.mkdirSync(path.join(tempDir, 'build'), { recursive: true });
fs.writeFileSync(path.join(tempDir, 'src', 'real.ts'), 'export const x = 1;');
fs.writeFileSync(path.join(tempDir, 'build', 'out.ts'), 'export const y = 2;');
// `\\[` makes the matcher build an unterminated character class — the throw
// is lazy (at match time), which is what escaped and killed sync.
fs.writeFileSync(path.join(tempDir, '.gitignore'), 'build/\n\\\\[\n');
let files: string[] = [];
expect(() => {
files = scanDirectory(tempDir);
}).not.toThrow();
expect(files).toContain('src/real.ts');
// The still-valid `build/` rule is honored; only the bad line was dropped.
expect(files.some((f) => f.startsWith('build/'))).toBe(false);
});
// A .gitignore that isn't valid UTF-8 — e.g. encrypted in place by corporate
// DLP / endpoint software (UTF-16 header + ciphertext) — is skipped whole,
// not fed to the matcher as garbage patterns (#682).
it('does not crash on a non-UTF-8 (DLP-encrypted) .gitignore (#682)', () => {
fs.mkdirSync(path.join(tempDir, 'src'), { recursive: true });
fs.writeFileSync(path.join(tempDir, 'src', 'real.ts'), 'export const x = 1;');
const header = Buffer.concat([
Buffer.from([0x00, 0x00]),
Buffer.from('[notice][user]', 'utf16le'),
]);
const junk = Buffer.from([0x5b, 0x99, 0xc3, 0x28, 0x5c, 0x5b, 0xff, 0xfd]);
fs.writeFileSync(path.join(tempDir, '.gitignore'), Buffer.concat([header, junk]));
let files: string[] = [];
expect(() => {
files = scanDirectory(tempDir);
}).not.toThrow();
expect(files).toContain('src/real.ts');
});
it('buildDefaultIgnore survives a bad .gitignore and still applies valid rules (#682)', () => {
fs.writeFileSync(path.join(tempDir, '.gitignore'), 'dist/\n\\\\[\n');
const ig = buildDefaultIgnore(tempDir);
expect(() => ig.ignores('src/app.ts')).not.toThrow();
expect(ig.ignores('dist/')).toBe(true); // valid rule survives
expect(ig.ignores('src/app.ts')).toBe(false);
});
});
// =============================================================================