Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions changelog.d/unreleased/2825.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: fixed
issues:
- 2825
affected:
- src/CodeIndex/Lsp/LspServer.cs
- tests/CodeIndex.Tests/LspServerTests.cs
---

## English

- **LSP message framing now rejects oversized frames and header lines (#2825)** — `cdidx lsp` now caps `Content-Length` frames and individual header lines before renting payload buffers.

## 日本語

- **LSP message framing が過大な frame と header line を拒否するようになりました (#2825)** — `cdidx lsp` は payload buffer を確保する前に `Content-Length` frame と個別 header line の上限を適用します。
16 changes: 16 additions & 0 deletions changelog.d/unreleased/2826.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: fixed
issues:
- 2826
affected:
- src/CodeIndex/Lsp/LspServer.cs
- tests/CodeIndex.Tests/LspServerTests.cs
---

## English

- **LSP position requests now stay inside indexed project files (#2826)** — `definition` and `references` now ignore unindexed, outside-root, or oversized documents and read only the requested line instead of materializing the whole file.

## 日本語

- **LSP position request が indexed project file 内に制限されました (#2826)** — `definition` / `references` は未 index、project root 外、または過大な document を無視し、ファイル全体を materialize せず要求行だけを読み取ります。
9 changes: 8 additions & 1 deletion src/CodeIndex/Cli/ProgramRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1569,7 +1569,14 @@ private static int RunLsp(string[] cmdArgs, string appVersion, JsonSerializerOpt
}

db.TryMigrateForRead();
using var server = new LspServer(new DbReader(db), appVersion, jsonOptions, db.GetMetaString(DbContext.IndexedProjectRootMetaKey));
var indexedProjectRoot = db.GetMetaString(DbContext.IndexedProjectRootMetaKey);
if (!string.IsNullOrWhiteSpace(indexedProjectRoot)
&& bool.TryParse(db.GetMetaString(DbContext.WorkspacePathCaseSensitiveMetaKey), out var pathCaseSensitive))
{
PathCasing.SeedFromWorkspace(indexedProjectRoot, ignoreCase: !pathCaseSensitive);
}

using var server = new LspServer(new DbReader(db), appVersion, jsonOptions, indexedProjectRoot);
server.Run(Console.OpenStandardInput(), Console.OpenStandardOutput());
return CommandExitCodes.Success;
}
Expand Down
171 changes: 161 additions & 10 deletions src/CodeIndex/Lsp/LspServer.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System.Buffers;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using CodeIndex.Cli;
using CodeIndex.Database;
using CodeIndex.Models;

Expand All @@ -10,10 +12,14 @@ namespace CodeIndex.Lsp;
internal sealed class LspServer : IDisposable
{
private const int DefaultLimit = 50;
internal const int MaxLspFrameBytes = 8 * 1024 * 1024;
internal const int MaxLspHeaderLineBytes = 8 * 1024;
internal const int MaxPositionDocumentBytes = 4 * 1024 * 1024;
private readonly DbReader _reader;
private readonly string _version;
private readonly JsonSerializerOptions _jsonOptions;
private readonly string? _projectRoot;
private readonly StringComparison _pathStringComparison;
private bool _shutdownRequested;

public LspServer(DbReader reader, string version, JsonSerializerOptions jsonOptions, string? projectRoot = null)
Expand All @@ -22,6 +28,7 @@ public LspServer(DbReader reader, string version, JsonSerializerOptions jsonOpti
_version = version;
_jsonOptions = jsonOptions;
_projectRoot = string.IsNullOrWhiteSpace(projectRoot) ? null : projectRoot;
_pathStringComparison = PathCasing.ComparisonFor(_projectRoot ?? Environment.CurrentDirectory);
}

public void Run(Stream input, Stream output)
Expand Down Expand Up @@ -147,15 +154,50 @@ private JsonArray References(JsonElement root)
if (line < 0 || character < 0)
return null;

var resolved = Path.IsPathRooted(path) ? path : Path.GetFullPath(path);
if (!File.Exists(resolved))
if (!TryResolveDocumentPath(path, out var resolvedPath, out var projectRelativePath))
return null;

var lines = File.ReadAllLines(resolved);
if (line >= lines.Length)
var indexedPath = ResolveIndexedPath(path, resolvedPath, projectRelativePath);
if (indexedPath == null || !TryResolveIndexedFilePath(indexedPath, out var indexedFullPath))
return null;

return ExtractTokenAtUtf16Position(lines[line], character);
if (!string.Equals(resolvedPath, indexedFullPath, _pathStringComparison))
return null;

if (!TryReadPositionLine(indexedFullPath, line, out var sourceLine))
return null;

return ExtractTokenAtUtf16Position(sourceLine, character);
}

private static bool TryReadPositionLine(string path, int targetLine, out string sourceLine)
{
sourceLine = string.Empty;
try
{
using var stream = File.OpenRead(path);
if (stream.Length > MaxPositionDocumentBytes)
return false;

using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
for (var currentLine = 0; currentLine <= targetLine; currentLine++)
{
var line = reader.ReadLine();
if (line == null)
return false;
if (currentLine == targetLine)
{
sourceLine = line;
return true;
}
}
}
catch (Exception ex) when (ex is ArgumentException or IOException or NotSupportedException or UnauthorizedAccessException)
{
return false;
}

return false;
}

internal static string? ExtractTokenAtUtf16Position(string line, int character)
Expand All @@ -181,27 +223,126 @@ private JsonArray References(JsonElement root)

private static bool IsTokenChar(char c) => char.IsLetterOrDigit(c) || c == '_' || c == '@';

private static bool MatchesDocumentPath(string indexedPath, string documentPath)
private bool MatchesDocumentPath(string indexedPath, string documentPath, string? projectRelativePath)
{
var normalizedIndexed = indexedPath.Replace('\\', '/');
if (_projectRoot != null)
{
if (Path.IsPathRooted(indexedPath)
&& TryResolveIndexedFilePath(indexedPath, out var indexedFullPath)
&& TryGetProjectRelativePath(indexedFullPath, out var indexedRelativePath)
&& indexedRelativePath != null)
{
normalizedIndexed = indexedRelativePath.Replace('\\', '/');
}

return projectRelativePath != null
&& string.Equals(normalizedIndexed, projectRelativePath.Replace('\\', '/'), _pathStringComparison);
}

if (string.Equals(indexedPath, documentPath, StringComparison.Ordinal))
return true;

var normalizedIndexed = indexedPath.Replace('\\', '/');
var normalizedDocument = documentPath.Replace('\\', '/');
return normalizedDocument.EndsWith("/" + normalizedIndexed, StringComparison.Ordinal);
}

private string? ResolveIndexedPath(string documentPath)
{
if (!TryResolveDocumentPath(documentPath, out var resolvedPath, out var projectRelativePath))
return null;

return ResolveIndexedPath(documentPath, resolvedPath, projectRelativePath);
}

private string? ResolveIndexedPath(string documentPath, string resolvedPath, string? projectRelativePath)
{
if (projectRelativePath != null)
{
var exactPath = projectRelativePath.Replace('\\', '/');
var exactFile = _reader.GetFileByPath(exactPath);
if (exactFile != null)
return exactFile.Path;
}

var fileName = Path.GetFileName(documentPath);
if (string.IsNullOrEmpty(fileName))
fileName = Path.GetFileName(resolvedPath);
if (string.IsNullOrEmpty(fileName))
return null;

var files = _reader.ListFiles(fileName, 1000);
var matches = files
.Where(file => MatchesDocumentPath(file.Path, documentPath))
.Where(file => MatchesDocumentPath(file.Path, documentPath, projectRelativePath))
.Take(2)
.ToList();
return matches.Count == 1 ? matches[0].Path : null;
}

private bool TryResolveDocumentPath(string documentPath, out string resolvedPath, out string? projectRelativePath)
{
resolvedPath = string.Empty;
projectRelativePath = null;
try
{
resolvedPath = Path.IsPathRooted(documentPath)
? Path.GetFullPath(documentPath)
: Path.GetFullPath(documentPath, _projectRoot ?? Environment.CurrentDirectory);
}
catch (Exception ex) when (ex is ArgumentException or IOException or NotSupportedException or UnauthorizedAccessException)
{
return false;
}

if (_projectRoot == null)
return true;

return TryGetProjectRelativePath(resolvedPath, out projectRelativePath);
}

private bool TryResolveIndexedFilePath(string indexedPath, out string resolvedPath)
{
resolvedPath = string.Empty;
try
{
resolvedPath = Path.IsPathRooted(indexedPath)
? Path.GetFullPath(indexedPath)
: Path.GetFullPath(indexedPath, _projectRoot ?? Environment.CurrentDirectory);
return true;
}
catch (Exception ex) when (ex is ArgumentException or IOException or NotSupportedException or UnauthorizedAccessException)
{
return false;
}
}

private bool TryGetProjectRelativePath(string resolvedPath, out string? relativePath)
{
relativePath = null;
if (_projectRoot == null)
return false;

try
{
var relative = Path.GetRelativePath(Path.GetFullPath(_projectRoot), resolvedPath);
if (relative == "."
|| relative == ".."
|| relative.StartsWith(".." + Path.DirectorySeparatorChar, StringComparison.Ordinal)
|| relative.StartsWith(".." + Path.AltDirectorySeparatorChar, StringComparison.Ordinal)
|| Path.IsPathRooted(relative))
{
return false;
}

relativePath = relative;
return true;
}
catch (Exception ex) when (ex is ArgumentException or IOException or NotSupportedException or UnauthorizedAccessException)
{
return false;
}
}

private JsonObject ToWorkspaceSymbol(SymbolResult symbol) => new()
{
["name"] = symbol.Name,
Expand Down Expand Up @@ -333,9 +474,15 @@ internal static bool TryReadMessage(Stream input, out string payload)
continue;
var name = line[..colon].Trim();
var value = line[(colon + 1)..].Trim();
if (string.Equals(name, "Content-Length", StringComparison.OrdinalIgnoreCase)
&& int.TryParse(value, out var parsed))
if (string.Equals(name, "Content-Length", StringComparison.OrdinalIgnoreCase))
{
if (!int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out var parsed)
|| parsed < 0
|| parsed > MaxLspFrameBytes)
{
return false;
}

contentLength = parsed;
}
}
Expand Down Expand Up @@ -383,7 +530,11 @@ internal static void WriteMessage(Stream output, string payload)
if (value == '\n')
break;
if (value != '\r')
{
if (bytes.Count >= MaxLspHeaderLineBytes)
return null;
bytes.Add((byte)value);
}
}
return Encoding.ASCII.GetString(bytes.ToArray());
}
Expand Down
Loading
Loading