/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/
using GitHub.Copilot.Rpc;
using System.Text.Json;
namespace GitHub.Copilot;
///
/// Result of a SQLite query execution via .
/// Same shape as but without the Error field,
/// since providers signal errors by throwing.
///
public sealed class SessionFsSqliteResult
{
/// Column names from the result set.
public IList Columns { get; set; } = [];
/// For SELECT: rows as column-keyed dictionaries. For others: empty.
public IList> Rows { get; set; } = [];
/// Number of rows affected (for INSERT/UPDATE/DELETE).
public long RowsAffected { get; set; }
/// Last inserted row ID (for INSERT).
public long? LastInsertRowid { get; set; }
}
///
/// Optional interface for subclasses that support
/// per-session SQLite databases. Implement this interface on your provider to enable
/// the runtime's SQL tool to route queries through your SessionFs implementation.
///
public interface ISessionFsSqliteProvider
{
///
/// Executes a SQLite query against the per-session database.
///
/// How to execute: "exec" for DDL/multi-statement, "query" for SELECT, "run" for INSERT/UPDATE/DELETE.
/// SQL query to execute.
/// Optional named bind parameters.
/// Cancellation token.
/// The query result, or null for exec-type queries.
Task QueryAsync(
SessionFsSqliteQueryType queryType,
string query,
IDictionary? bindParams,
CancellationToken cancellationToken);
///
/// Checks whether the per-session SQLite database already exists, without creating it.
///
/// Cancellation token.
Task ExistsAsync(CancellationToken cancellationToken);
}
///
/// Base class for session filesystem providers. Subclasses override the
/// virtual methods and use normal C# patterns (return values, throw exceptions).
/// The base class catches exceptions and converts them to
/// results expected by the runtime.
/// To add SQLite support, also implement .
///
public abstract class SessionFsProvider : ISessionFsHandler
{
/// Reads the full content of a file. Throw if the file does not exist.
/// SessionFs-relative path.
/// Cancellation token.
/// The file content as a UTF-8 string.
protected abstract Task ReadFileAsync(string path, CancellationToken cancellationToken);
/// Writes content to a file, creating it (and parent directories) if needed.
/// SessionFs-relative path.
/// Content to write.
/// Optional POSIX-style permission mode. Null means use OS default.
/// Cancellation token.
protected abstract Task WriteFileAsync(string path, string content, int? mode, CancellationToken cancellationToken);
/// Appends content to a file, creating it (and parent directories) if needed.
/// SessionFs-relative path.
/// Content to append.
/// Optional POSIX-style permission mode. Null means use OS default.
/// Cancellation token.
protected abstract Task AppendFileAsync(string path, string content, int? mode, CancellationToken cancellationToken);
/// Checks whether a path exists.
/// SessionFs-relative path.
/// Cancellation token.
/// true if the path exists, false otherwise.
protected abstract Task ExistsAsync(string path, CancellationToken cancellationToken);
/// Gets metadata about a file or directory. Throw if the path does not exist.
/// SessionFs-relative path.
/// Cancellation token.
protected abstract Task StatAsync(string path, CancellationToken cancellationToken);
/// Creates a directory (and optionally parents). Does not fail if it already exists.
/// SessionFs-relative path.
/// Whether to create parent directories.
/// Optional POSIX-style permission mode (e.g., 0x1FF for 0777). Null means use OS default.
/// Cancellation token.
protected abstract Task MakeDirectoryAsync(string path, bool recursive, int? mode, CancellationToken cancellationToken);
/// Lists entry names in a directory. Throw if the directory does not exist.
/// SessionFs-relative path.
/// Cancellation token.
protected abstract Task> ReadDirectoryAsync(string path, CancellationToken cancellationToken);
/// Lists entries with type info in a directory. Throw if the directory does not exist.
/// SessionFs-relative path.
/// Cancellation token.
protected abstract Task> ReadDirectoryWithTypesAsync(string path, CancellationToken cancellationToken);
/// Removes a file or directory. Throw if the path does not exist (unless is true).
/// SessionFs-relative path.
/// Whether to remove directory contents recursively.
/// If true, do not throw when the path does not exist.
/// Cancellation token.
protected abstract Task RemoveAsync(string path, bool recursive, bool force, CancellationToken cancellationToken);
/// Renames/moves a file or directory.
/// Source path.
/// Destination path.
/// Cancellation token.
protected abstract Task RenameAsync(string src, string dest, CancellationToken cancellationToken);
// ---- ISessionFsHandler implementation (private, handles error mapping) ----
async Task ISessionFsHandler.ReadFileAsync(SessionFsReadFileRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
try
{
var content = await ReadFileAsync(request.Path, cancellationToken).ConfigureAwait(false);
return new SessionFsReadFileResult { Content = content };
}
catch (Exception ex)
{
return new SessionFsReadFileResult { Error = ToSessionFsError(ex) };
}
}
async Task ISessionFsHandler.WriteFileAsync(SessionFsWriteFileRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
try
{
await WriteFileAsync(request.Path, request.Content, (int?)request.Mode, cancellationToken).ConfigureAwait(false);
return null;
}
catch (Exception ex)
{
return ToSessionFsError(ex);
}
}
async Task ISessionFsHandler.AppendFileAsync(SessionFsAppendFileRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
try
{
await AppendFileAsync(request.Path, request.Content, (int?)request.Mode, cancellationToken).ConfigureAwait(false);
return null;
}
catch (Exception ex)
{
return ToSessionFsError(ex);
}
}
async Task ISessionFsHandler.ExistsAsync(SessionFsExistsRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
try
{
var exists = await ExistsAsync(request.Path, cancellationToken).ConfigureAwait(false);
return new SessionFsExistsResult { Exists = exists };
}
catch
{
return new SessionFsExistsResult { Exists = false };
}
}
async Task ISessionFsHandler.StatAsync(SessionFsStatRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
try
{
return await StatAsync(request.Path, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
return new SessionFsStatResult { Error = ToSessionFsError(ex) };
}
}
async Task ISessionFsHandler.MkdirAsync(SessionFsMkdirRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
try
{
await MakeDirectoryAsync(request.Path, request.Recursive ?? false, (int?)request.Mode, cancellationToken).ConfigureAwait(false);
return null;
}
catch (Exception ex)
{
return ToSessionFsError(ex);
}
}
async Task ISessionFsHandler.ReaddirAsync(SessionFsReaddirRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
try
{
var entries = await ReadDirectoryAsync(request.Path, cancellationToken).ConfigureAwait(false);
return new SessionFsReaddirResult { Entries = entries };
}
catch (Exception ex)
{
return new SessionFsReaddirResult { Error = ToSessionFsError(ex) };
}
}
async Task ISessionFsHandler.ReaddirWithTypesAsync(SessionFsReaddirWithTypesRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
try
{
var entries = await ReadDirectoryWithTypesAsync(request.Path, cancellationToken).ConfigureAwait(false);
return new SessionFsReaddirWithTypesResult { Entries = entries };
}
catch (Exception ex)
{
return new SessionFsReaddirWithTypesResult { Error = ToSessionFsError(ex) };
}
}
async Task ISessionFsHandler.RmAsync(SessionFsRmRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
try
{
await RemoveAsync(request.Path, request.Recursive ?? false, request.Force ?? false, cancellationToken).ConfigureAwait(false);
return null;
}
catch (Exception ex)
{
return ToSessionFsError(ex);
}
}
async Task ISessionFsHandler.RenameAsync(SessionFsRenameRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
try
{
await RenameAsync(request.Src, request.Dest, cancellationToken).ConfigureAwait(false);
return null;
}
catch (Exception ex)
{
return ToSessionFsError(ex);
}
}
async Task ISessionFsHandler.SqliteQueryAsync(SessionFsSqliteQueryRequest request, CancellationToken cancellationToken)
{
if (this is not ISessionFsSqliteProvider sqliteProvider)
{
return new SessionFsSqliteQueryResult
{
Error = new SessionFsError { Code = SessionFsErrorCode.UNKNOWN, Message = "SQLite is not supported by this provider." },
};
}
try
{
var bindParams = request.Params?.ToDictionary(
kvp => kvp.Key,
kvp => JsonElementToValue(kvp.Value));
var result = await sqliteProvider.QueryAsync(request.QueryType, request.Query, bindParams, cancellationToken).ConfigureAwait(false);
return new SessionFsSqliteQueryResult
{
Rows = result?.Rows?.Select(row => (IDictionary)row.ToDictionary(
kvp => kvp.Key,
kvp => CopilotClient.ToJsonElementForWire(kvp.Value)!.Value)).ToList() ?? [],
Columns = result?.Columns ?? [],
RowsAffected = result?.RowsAffected ?? 0,
LastInsertRowid = result?.LastInsertRowid,
};
}
catch (Exception ex)
{
return new SessionFsSqliteQueryResult { Error = ToSessionFsError(ex) };
}
}
async Task ISessionFsHandler.SqliteExistsAsync(SessionFsSqliteExistsRequest request, CancellationToken cancellationToken)
{
if (this is not ISessionFsSqliteProvider sqliteProvider)
{
return new SessionFsSqliteExistsResult { Exists = false };
}
try
{
var exists = await sqliteProvider.ExistsAsync(cancellationToken).ConfigureAwait(false);
return new SessionFsSqliteExistsResult { Exists = exists };
}
catch
{
return new SessionFsSqliteExistsResult { Exists = false };
}
}
private static SessionFsError ToSessionFsError(Exception ex)
{
var code = ex is FileNotFoundException or DirectoryNotFoundException
? SessionFsErrorCode.ENOENT
: SessionFsErrorCode.UNKNOWN;
return new SessionFsError { Code = code, Message = ex.Message };
}
private static object? JsonElementToValue(JsonElement element) => element.ValueKind switch
{
JsonValueKind.Null => null,
JsonValueKind.True => true,
JsonValueKind.False => false,
JsonValueKind.String => element.GetString(),
JsonValueKind.Number => element.TryGetInt64(out var l) ? l : element.GetDouble(),
_ => element.GetRawText(),
};
}