From 331cc2bd61b77d83edfba37ca5138e06b1432151 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Tue, 2 Jun 2026 10:13:04 +0900 Subject: [PATCH 1/5] Validate import archive manifests (#2834) --- changelog.d/unreleased/2834.security.md | 16 ++++ .../Cli/ExportImportCommandRunner.cs | 96 ++++++++++++++++++- tests/CodeIndex.Tests/ProgramCliTests.cs | 95 ++++++++++++++++++ 3 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 changelog.d/unreleased/2834.security.md diff --git a/changelog.d/unreleased/2834.security.md b/changelog.d/unreleased/2834.security.md new file mode 100644 index 0000000000..5fe92d037f --- /dev/null +++ b/changelog.d/unreleased/2834.security.md @@ -0,0 +1,16 @@ +--- +category: security +issues: + - 2834 +affected: + - src/CodeIndex/Cli/ExportImportCommandRunner.cs + - tests/CodeIndex.Tests/ProgramCliTests.cs +--- + +## English + +- **`cdidx import` now verifies archive manifest hashes (#2834)** — import reads `manifest.json`, rejects unsupported manifest/schema versions, and compares `database_sha256` against the embedded `codeindex.db` before installing the database. + +## 日本語 + +- **`cdidx import` がarchive manifestのhashを検証するようになりました (#2834)** — import は `manifest.json` を読み取り、未対応のmanifest/schema versionを拒否し、databaseを配置する前に `database_sha256` と内包された `codeindex.db` を照合します。 diff --git a/src/CodeIndex/Cli/ExportImportCommandRunner.cs b/src/CodeIndex/Cli/ExportImportCommandRunner.cs index 05b5150906..714421c0b1 100644 --- a/src/CodeIndex/Cli/ExportImportCommandRunner.cs +++ b/src/CodeIndex/Cli/ExportImportCommandRunner.cs @@ -1,4 +1,5 @@ using System.IO.Compression; +using System.Globalization; using System.Security.Cryptography; using System.Text; using System.Text.Json; @@ -73,14 +74,22 @@ public static int RunImport(string[] args, JsonSerializerOptions jsonOptions) Directory.CreateDirectory(dbDirectory); using (var archive = ZipFile.OpenRead(archivePath)) { - if (archive.GetEntry(ManifestEntryName) == null) + var manifestEntry = archive.GetEntry(ManifestEntryName); + if (manifestEntry == null) return WriteError("archive is missing manifest.json.", "use an archive produced by `cdidx export `.", "cdidx import [--db ] [--json]"); + if (!TryReadManifest(manifestEntry, jsonOptions, out var manifest, out var manifestError)) + return WriteError($"archive manifest is invalid: {manifestError}.", "use an archive produced by `cdidx export `.", "cdidx import [--db ] [--json]"); + if (!TryValidateManifestHeader(manifest, out var manifestHeaderError)) + return WriteError($"archive manifest is invalid: {manifestHeaderError}.", "re-export from a compatible CodeIndex database.", "cdidx import [--db ] [--json]"); var dbEntry = archive.GetEntry(DatabaseEntryName); if (dbEntry == null) return WriteError("archive is missing codeindex.db.", "use an archive produced by `cdidx export `.", "cdidx import [--db ] [--json]"); dbEntry.ExtractToFile(tempPath, overwrite: true); + + if (!TryValidateImportedManifest(manifest, tempPath, out var manifestValidationMessage)) + return WriteError($"archive manifest mismatch: {manifestValidationMessage}.", "re-export from a compatible CodeIndex database.", "cdidx import [--db ] [--prune-paths] [--json]"); } if (!DbContext.TryValidateExistingCodeIndexDb(tempPath, out var validationMessage, out _)) @@ -306,6 +315,91 @@ private static string ComputeSha256(string path) return Convert.ToHexString(SHA256.HashData(stream)).ToLowerInvariant(); } + private static bool TryReadManifest(ZipArchiveEntry manifestEntry, JsonSerializerOptions jsonOptions, out ExportManifest manifest, out string message) + { + try + { + using var stream = manifestEntry.Open(); + manifest = JsonSerializer.Deserialize(stream, jsonOptions) + ?? throw new JsonException("manifest.json did not contain an object"); + message = string.Empty; + return true; + } + catch (Exception ex) when (ex is JsonException or NotSupportedException) + { + manifest = null!; + message = ex.Message; + return false; + } + } + + private static bool TryValidateManifestHeader(ExportManifest manifest, out string message) + { + if (!string.Equals(manifest.FormatVersion, "1", StringComparison.Ordinal)) + { + message = $"unsupported format_version `{manifest.FormatVersion}`"; + return false; + } + + if (manifest.UserVersion < 0 || (manifest.UserVersion & ~DbContext.CurrentSchemaVersion) != 0) + { + message = $"unsupported user_version `{manifest.UserVersion}`"; + return false; + } + + if (!IsSha256Hex(manifest.DatabaseSha256)) + { + message = "database_sha256 is missing or invalid"; + return false; + } + + message = string.Empty; + return true; + } + + private static bool TryValidateImportedManifest(ExportManifest manifest, string dbPath, out string message) + { + var actualSha256 = ComputeSha256(dbPath); + if (!string.Equals(manifest.DatabaseSha256, actualSha256, StringComparison.OrdinalIgnoreCase)) + { + message = "database_sha256 does not match codeindex.db"; + return false; + } + + var actualUserVersion = ReadSqliteUserVersion(dbPath); + if (actualUserVersion != manifest.UserVersion) + { + message = $"manifest user_version `{manifest.UserVersion}` does not match codeindex.db user_version `{actualUserVersion}`"; + return false; + } + + message = string.Empty; + return true; + } + + private static int ReadSqliteUserVersion(string dbPath) + { + using var connection = new SqliteConnection(CreateUnpooledConnectionString(dbPath)); + connection.Open(); + using var cmd = connection.CreateCommand(); + cmd.CommandText = "PRAGMA user_version"; + return Convert.ToInt32(cmd.ExecuteScalar(), CultureInfo.InvariantCulture); + } + + private static bool IsSha256Hex(string? value) + { + if (value == null || value.Length != 64) + return false; + + foreach (var ch in value) + { + if (!char.IsAsciiHexDigit(ch)) + return false; + } + + return true; + } + private static void RewriteImportedProjectRoot(string dbPath, string projectRoot) { using var connection = new SqliteConnection(CreateUnpooledConnectionString(dbPath)); diff --git a/tests/CodeIndex.Tests/ProgramCliTests.cs b/tests/CodeIndex.Tests/ProgramCliTests.cs index df1d8dfe4d..d515c2f832 100644 --- a/tests/CodeIndex.Tests/ProgramCliTests.cs +++ b/tests/CodeIndex.Tests/ProgramCliTests.cs @@ -2,6 +2,7 @@ using CodeIndex.Database; using CodeIndex.Models; using Microsoft.Data.Sqlite; +using System.IO.Compression; using System.Text.Json; namespace CodeIndex.Tests; @@ -373,6 +374,62 @@ public void ExportImportArchive_RestoresCodeIndexDatabase() } } + [Fact] + public void ImportArchive_RejectsDatabaseHashMismatch() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_import_hash_mismatch"); + var replacementRoot = TestProjectHelper.CreateTempProject("cdidx_import_hash_replacement"); + try + { + var sourceDbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(sourceDbPath, "src/app.cs", "csharp", "class App { void Run() {} }\n"); + var replacementDbPath = TestProjectHelper.CreateProjectDb(replacementRoot); + TestProjectHelper.InsertIndexedFile(replacementDbPath, "src/other.cs", "csharp", "class Other { void Run() {} }\n"); + var archivePath = Path.Combine(projectRoot, "codeindex.cdidx.zip"); + var importedDbPath = Path.Combine(projectRoot, "imported", "codeindex.db"); + + var (exportExit, _, exportStderr) = RunCliInSubprocess(["export", archivePath, "--db", sourceDbPath]); + ReplaceZipEntryWithFile(archivePath, "codeindex.db", replacementDbPath); + var (importExit, _, importStderr) = RunCliInSubprocess(["import", archivePath, "--db", importedDbPath]); + + Assert.True(exportExit == 0, exportStderr); + Assert.Equal(CommandExitCodes.UsageError, importExit); + Assert.Contains("database_sha256 does not match codeindex.db", importStderr); + Assert.False(File.Exists(importedDbPath)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + TestProjectHelper.DeleteDirectory(replacementRoot); + } + } + + [Fact] + public void ImportArchive_RejectsManifestUserVersionMismatch() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_import_user_version_mismatch"); + try + { + var sourceDbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(sourceDbPath, "src/app.cs", "csharp", "class App { void Run() {} }\n"); + var archivePath = Path.Combine(projectRoot, "codeindex.cdidx.zip"); + var importedDbPath = Path.Combine(projectRoot, "imported", "codeindex.db"); + + var (exportExit, _, exportStderr) = RunCliInSubprocess(["export", archivePath, "--db", sourceDbPath]); + ReplaceManifestUserVersion(archivePath, newUserVersion: 1); + var (importExit, _, importStderr) = RunCliInSubprocess(["import", archivePath, "--db", importedDbPath]); + + Assert.True(exportExit == 0, exportStderr); + Assert.Equal(CommandExitCodes.UsageError, importExit); + Assert.Contains("user_version", importStderr); + Assert.False(File.Exists(importedDbPath)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + [Fact] public void ExportArchive_RejectsSourceDatabaseAsOutput() { @@ -703,6 +760,44 @@ private static string GetRepositoryRoot() throw new InvalidOperationException("Could not locate repository root / リポジトリルートを特定できませんでした"); } + private static void ReplaceZipEntryWithFile(string archivePath, string entryName, string sourcePath) + { + using var archive = ZipFile.Open(archivePath, ZipArchiveMode.Update); + archive.GetEntry(entryName)?.Delete(); + var entry = archive.CreateEntry(entryName, CompressionLevel.SmallestSize); + using var source = File.OpenRead(sourcePath); + using var target = entry.Open(); + source.CopyTo(target); + } + + private static void ReplaceManifestUserVersion(string archivePath, int newUserVersion) + { + using var archive = ZipFile.Open(archivePath, ZipArchiveMode.Update); + var entry = archive.GetEntry("manifest.json") + ?? throw new InvalidOperationException("manifest.json entry was not found"); + + string manifestJson; + using (var reader = new StreamReader(entry.Open())) + { + manifestJson = reader.ReadToEnd(); + } + + using var document = JsonDocument.Parse(manifestJson); + var oldUserVersion = document.RootElement.GetProperty("user_version").GetInt32(); + var replacementUserVersion = newUserVersion == oldUserVersion + ? (oldUserVersion == 0 ? 1 : 0) + : newUserVersion; + var updatedManifestJson = manifestJson.Replace( + $"\"user_version\":{oldUserVersion}", + $"\"user_version\":{replacementUserVersion}", + StringComparison.Ordinal); + + entry.Delete(); + var replacementEntry = archive.CreateEntry("manifest.json", CompressionLevel.SmallestSize); + using var writer = new StreamWriter(replacementEntry.Open()); + writer.Write(updatedManifestJson); + } + private sealed class SuggestionFixture : IDisposable { private readonly string _root; From 7fbdbc54ff9fc0712a3ee3c973d196318f731ac5 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Tue, 2 Jun 2026 10:17:53 +0900 Subject: [PATCH 2/5] Bound import archive database extraction (#2835) --- changelog.d/unreleased/2835.security.md | 16 ++++++ .../Cli/ExportImportCommandRunner.cs | 54 ++++++++++++++++++- .../ExportImportCommandRunnerTests.cs | 54 +++++++++++++++++++ 3 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 changelog.d/unreleased/2835.security.md create mode 100644 tests/CodeIndex.Tests/ExportImportCommandRunnerTests.cs diff --git a/changelog.d/unreleased/2835.security.md b/changelog.d/unreleased/2835.security.md new file mode 100644 index 0000000000..a44ca841f3 --- /dev/null +++ b/changelog.d/unreleased/2835.security.md @@ -0,0 +1,16 @@ +--- +category: security +issues: + - 2835 +affected: + - src/CodeIndex/Cli/ExportImportCommandRunner.cs + - tests/CodeIndex.Tests/ExportImportCommandRunnerTests.cs +--- + +## English + +- **`cdidx import` now bounds archive database extraction (#2835)** — import rejects oversized `codeindex.db` entries from archive metadata and stops stream extraction if the database grows past the fixed import limit. + +## 日本語 + +- **`cdidx import` がarchive databaseの展開サイズを制限するようになりました (#2835)** — import はarchive metadata上の過大な `codeindex.db` entryを拒否し、databaseが固定のimport上限を超えた場合はstream展開中にも停止します。 diff --git a/src/CodeIndex/Cli/ExportImportCommandRunner.cs b/src/CodeIndex/Cli/ExportImportCommandRunner.cs index 714421c0b1..b7929b6dd4 100644 --- a/src/CodeIndex/Cli/ExportImportCommandRunner.cs +++ b/src/CodeIndex/Cli/ExportImportCommandRunner.cs @@ -12,6 +12,8 @@ internal static class ExportImportCommandRunner { private const string ManifestEntryName = "manifest.json"; private const string DatabaseEntryName = "codeindex.db"; + internal const long MaxImportDatabaseBytes = 8L * 1024 * 1024 * 1024; + private const int ImportCopyBufferSize = 81920; private static readonly DateTimeOffset DeterministicZipTimestamp = new(1980, 1, 1, 0, 0, 0, TimeSpan.Zero); public static int RunExport(string[] args, JsonSerializerOptions jsonOptions, string appVersion) @@ -85,8 +87,10 @@ public static int RunImport(string[] args, JsonSerializerOptions jsonOptions) var dbEntry = archive.GetEntry(DatabaseEntryName); if (dbEntry == null) return WriteError("archive is missing codeindex.db.", "use an archive produced by `cdidx export `.", "cdidx import [--db ] [--json]"); + if (!TryValidateDatabaseEntrySize(dbEntry.Length, dbEntry.CompressedLength, out var sizeValidationMessage)) + return WriteError(sizeValidationMessage, "re-export a smaller CodeIndex database or rebuild a smaller index.", "cdidx import [--db ] [--prune-paths] [--json]"); - dbEntry.ExtractToFile(tempPath, overwrite: true); + ExtractDatabaseEntryToFile(dbEntry, tempPath); if (!TryValidateImportedManifest(manifest, tempPath, out var manifestValidationMessage)) return WriteError($"archive manifest mismatch: {manifestValidationMessage}.", "re-export from a compatible CodeIndex database.", "cdidx import [--db ] [--prune-paths] [--json]"); @@ -400,6 +404,54 @@ private static bool IsSha256Hex(string? value) return true; } + internal static bool TryValidateDatabaseEntrySize(long uncompressedLength, long compressedLength, out string message) + { + if (uncompressedLength < 0 || compressedLength < 0) + { + message = "archive codeindex.db size metadata is invalid"; + return false; + } + + if (uncompressedLength > MaxImportDatabaseBytes) + { + message = $"archive codeindex.db is too large: {ConsoleUi.FormatBytes(uncompressedLength)} uncompressed exceeds the import limit of {ConsoleUi.FormatBytes(MaxImportDatabaseBytes)}"; + return false; + } + + if (compressedLength > MaxImportDatabaseBytes) + { + message = $"archive codeindex.db is too large: {ConsoleUi.FormatBytes(compressedLength)} compressed exceeds the import limit of {ConsoleUi.FormatBytes(MaxImportDatabaseBytes)}"; + return false; + } + + message = string.Empty; + return true; + } + + private static void ExtractDatabaseEntryToFile(ZipArchiveEntry dbEntry, string destinationPath) + { + using var source = dbEntry.Open(); + using var target = File.Open(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None); + CopyToWithLimit(source, target, MaxImportDatabaseBytes); + } + + internal static long CopyToWithLimit(Stream source, Stream target, long maxBytes) + { + var buffer = new byte[ImportCopyBufferSize]; + long totalBytes = 0; + int bytesRead; + while ((bytesRead = source.Read(buffer, 0, buffer.Length)) > 0) + { + if (totalBytes > maxBytes - bytesRead) + throw new InvalidDataException($"archive codeindex.db exceeds the import limit of {ConsoleUi.FormatBytes(maxBytes)}."); + + target.Write(buffer, 0, bytesRead); + totalBytes += bytesRead; + } + + return totalBytes; + } + private static void RewriteImportedProjectRoot(string dbPath, string projectRoot) { using var connection = new SqliteConnection(CreateUnpooledConnectionString(dbPath)); diff --git a/tests/CodeIndex.Tests/ExportImportCommandRunnerTests.cs b/tests/CodeIndex.Tests/ExportImportCommandRunnerTests.cs new file mode 100644 index 0000000000..e99de47670 --- /dev/null +++ b/tests/CodeIndex.Tests/ExportImportCommandRunnerTests.cs @@ -0,0 +1,54 @@ +using CodeIndex.Cli; + +namespace CodeIndex.Tests; + +public class ExportImportCommandRunnerTests +{ + [Fact] + public void TryValidateDatabaseEntrySize_RejectsOversizedUncompressedLength() + { + var ok = ExportImportCommandRunner.TryValidateDatabaseEntrySize( + uncompressedLength: ExportImportCommandRunner.MaxImportDatabaseBytes + 1, + compressedLength: 1, + message: out var message); + + Assert.False(ok); + Assert.Contains("uncompressed exceeds the import limit", message); + } + + [Fact] + public void TryValidateDatabaseEntrySize_RejectsOversizedCompressedLength() + { + var ok = ExportImportCommandRunner.TryValidateDatabaseEntrySize( + uncompressedLength: 1, + compressedLength: ExportImportCommandRunner.MaxImportDatabaseBytes + 1, + message: out var message); + + Assert.False(ok); + Assert.Contains("compressed exceeds the import limit", message); + } + + [Fact] + public void CopyToWithLimit_ThrowsBeforeWritingPastLimit() + { + using var source = new MemoryStream([1, 2, 3, 4]); + using var target = new MemoryStream(); + + var ex = Assert.Throws(() => ExportImportCommandRunner.CopyToWithLimit(source, target, maxBytes: 3)); + + Assert.Contains("codeindex.db exceeds the import limit", ex.Message); + Assert.Equal(0, target.Length); + } + + [Fact] + public void CopyToWithLimit_AllowsExactLimit() + { + using var source = new MemoryStream([1, 2, 3, 4]); + using var target = new MemoryStream(); + + var copied = ExportImportCommandRunner.CopyToWithLimit(source, target, maxBytes: 4); + + Assert.Equal(4, copied); + Assert.Equal([1, 2, 3, 4], target.ToArray()); + } +} From 1d963bf1b8bffd1db8c190a662d6b3786d179286 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Tue, 2 Jun 2026 10:34:49 +0900 Subject: [PATCH 3/5] Document import manifest validation (#2834) --- DEVELOPER_GUIDE.md | 4 ++-- changelog.d/unreleased/2834.security.md | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 7ef4a341c2..03f998576a 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -92,8 +92,8 @@ consumer can run `cdidx import codeindex.cdidx.zip --db ` before query commands. Use `--prune-paths` on import when the archive comes from another checkout and the restored DB should advertise the current workspace root. The archive contains `manifest.json` plus `codeindex.db`; import validates the -embedded SQLite file as a CodeIndex database before replacing the destination -DB. +manifest format, manifest `user_version`, `database_sha256`, and embedded +SQLite file as a CodeIndex database before replacing the destination DB. Use `cdidx db checkpoint ` to take a filesystem snapshot of `codeindex.db` plus existing WAL/SHM sidecars before risky maintenance, and use diff --git a/changelog.d/unreleased/2834.security.md b/changelog.d/unreleased/2834.security.md index 5fe92d037f..cc864f029a 100644 --- a/changelog.d/unreleased/2834.security.md +++ b/changelog.d/unreleased/2834.security.md @@ -3,6 +3,7 @@ category: security issues: - 2834 affected: + - DEVELOPER_GUIDE.md - src/CodeIndex/Cli/ExportImportCommandRunner.cs - tests/CodeIndex.Tests/ProgramCliTests.cs --- From 8a771e8001c9e02ee78a3fae94de36ae067c5cc3 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Tue, 2 Jun 2026 10:36:37 +0900 Subject: [PATCH 4/5] Document import archive size limit (#2835) --- DEVELOPER_GUIDE.md | 2 ++ changelog.d/unreleased/2835.security.md | 1 + 2 files changed, 3 insertions(+) diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 03f998576a..b20af78d65 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -94,6 +94,8 @@ checkout and the restored DB should advertise the current workspace root. The archive contains `manifest.json` plus `codeindex.db`; import validates the manifest format, manifest `user_version`, `database_sha256`, and embedded SQLite file as a CodeIndex database before replacing the destination DB. +Import rejects archive `codeindex.db` entries whose compressed or uncompressed +metadata exceeds 8 GiB, and the extraction stream is also capped at 8 GiB. Use `cdidx db checkpoint ` to take a filesystem snapshot of `codeindex.db` plus existing WAL/SHM sidecars before risky maintenance, and use diff --git a/changelog.d/unreleased/2835.security.md b/changelog.d/unreleased/2835.security.md index a44ca841f3..1f7a991f12 100644 --- a/changelog.d/unreleased/2835.security.md +++ b/changelog.d/unreleased/2835.security.md @@ -3,6 +3,7 @@ category: security issues: - 2835 affected: + - DEVELOPER_GUIDE.md - src/CodeIndex/Cli/ExportImportCommandRunner.cs - tests/CodeIndex.Tests/ExportImportCommandRunnerTests.cs --- From bbc6933b07e5b46d53e49efd4074141d1b204415 Mon Sep 17 00:00:00 2001 From: Widthdom <125688807+Widthdom@users.noreply.github.com> Date: Tue, 2 Jun 2026 18:46:22 +0900 Subject: [PATCH 5/5] Fix Windows SQLite test file release (#2834) --- tests/CodeIndex.Tests/TestProjectHelper.cs | 54 ++++++++++++---------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/tests/CodeIndex.Tests/TestProjectHelper.cs b/tests/CodeIndex.Tests/TestProjectHelper.cs index 66d404de38..a66485368d 100644 --- a/tests/CodeIndex.Tests/TestProjectHelper.cs +++ b/tests/CodeIndex.Tests/TestProjectHelper.cs @@ -48,34 +48,38 @@ internal static void InsertIndexedFile(string dbPath, string path, string lang, var lines = normalized.Split('\n'); var lineCount = FileIndexer.CountPhysicalLines(content); - using var db = new DbContext(dbPath); - db.InitializeSchema(); - - var writer = new DbWriter(db.Connection); - var fileId = writer.UpsertFile(new FileRecord + using (var db = new DbContext(dbPath)) { - Path = path, - Lang = lang, - Size = normalized.Length, - Lines = lineCount, - Modified = modified ?? new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc), - Checksum = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(normalized))).ToLowerInvariant(), - }); - - writer.InsertChunks([ - new ChunkRecord + db.InitializeSchema(); + + var writer = new DbWriter(db.Connection); + var fileId = writer.UpsertFile(new FileRecord { - FileId = fileId, - ChunkIndex = 0, - StartLine = 1, - EndLine = lines.Length, - Content = normalized, - } - ]); + Path = path, + Lang = lang, + Size = normalized.Length, + Lines = lineCount, + Modified = modified ?? new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc), + Checksum = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(normalized))).ToLowerInvariant(), + }); + + writer.InsertChunks([ + new ChunkRecord + { + FileId = fileId, + ChunkIndex = 0, + StartLine = 1, + EndLine = lines.Length, + Content = normalized, + } + ]); + + var symbols = SymbolExtractor.Extract(fileId, lang, normalized, path); + writer.InsertSymbols(symbols); + writer.InsertReferences(ReferenceExtractor.Extract(fileId, lang, normalized, symbols)); + } - var symbols = SymbolExtractor.Extract(fileId, lang, normalized, path); - writer.InsertSymbols(symbols); - writer.InsertReferences(ReferenceExtractor.Extract(fileId, lang, normalized, symbols)); + SqlitePoolCleanup.ClearPoolsForWindowsFileRelease(); } internal static string RunGit(string workDir, params string[] args)