blob: cdfad13a90a159669ba81007e989628de9a4e17c [file] [log] [blame]
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:convert';
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:dtd/dtd.dart';
import 'package:json_rpc_2/json_rpc_2.dart';
import 'package:path/path.dart' as path;
import '../constants.dart';
import '../dtd_client.dart';
import 'internal_service.dart';
class FileSystemService extends InternalService {
FileSystemService({required this.secret, required this.unrestrictedMode});
final String secret;
final bool unrestrictedMode;
final List<Uri> _ideWorkspaceRoots = [];
@override
String get serviceName => FileSystemServiceConstants.serviceName;
static const int _defaultGetProjectRootsDepth = 4;
@override
void register(DTDClient client) {
client
..registerServiceMethod(
serviceName,
FileSystemServiceConstants.readFileAsString,
_readFileAsString,
)
..registerServiceMethod(
serviceName,
FileSystemServiceConstants.writeFileAsString,
_writeFileAsString,
)
..registerServiceMethod(
serviceName,
FileSystemServiceConstants.listDirectoryContents,
_listDirectoryContents,
)
..registerServiceMethod(
serviceName,
FileSystemServiceConstants.setIDEWorkspaceRoots,
_setIDEWorkspaceRoots,
)
..registerServiceMethod(
serviceName,
FileSystemServiceConstants.getIDEWorkspaceRoots,
_getIDEWorkspaceRoots,
)
..registerServiceMethod(
serviceName,
FileSystemServiceConstants.getProjectRoots,
_getProjectRoots,
);
}
@override
void shutdown() {
_ideWorkspaceRoots.clear();
}
void _ensureIDEWorkspaceRootsContainUri(Uri uri) {
// If in unrestricted mode, no need to do these checks.
if (unrestrictedMode) return;
final requestedPath = uri.toFilePath();
if (_ideWorkspaceRoots.any((root) {
final rootPath = root.toFilePath();
return path.isWithin(rootPath, requestedPath) ||
path.equals(rootPath, requestedPath);
})) {
return;
}
throw RpcErrorCodes.buildRpcException(
RpcErrorCodes.kPermissionDenied,
);
}
Map<String, Object?> _setIDEWorkspaceRoots(Parameters parameters) {
final incomingSecret = parameters[DtdParameters.secret].asString;
if (!unrestrictedMode && secret != incomingSecret) {
throw RpcErrorCodes.buildRpcException(
RpcErrorCodes.kPermissionDenied,
);
}
final newRoots = <Uri>[];
for (final root in parameters[DtdParameters.roots].asList.cast<String>()) {
final rootUri = Uri.parse(path.normalize(root));
if (rootUri.scheme != 'file') {
throw RpcErrorCodes.buildRpcException(
RpcErrorCodes.kExpectsUriParamWithFileScheme,
);
}
newRoots.add(rootUri);
}
_ideWorkspaceRoots.clear();
_ideWorkspaceRoots.addAll(newRoots);
return RPCResponses.success;
}
Map<String, Object?> _getIDEWorkspaceRoots(Parameters _) {
return IDEWorkspaceRoots(ideWorkspaceRoots: _ideWorkspaceRoots).toJson();
}
Future<Map<String, Object?>> _getProjectRoots(Parameters parameters) async {
final searchDepth =
parameters[DtdParameters.depth].asIntOr(_defaultGetProjectRootsDepth);
final projectRoots = <Uri>[];
// Recursive helper method to find all project roots within [directory], up
// to a maximum depth of [maxSearchDepth].
Future<void> findProjectRoots(
Directory dir, {
required int currentDepth,
}) async {
if (await dir.exists()) {
// Setting 'followLinks' to false means that any symbolic links returned
// in this list will have type [Link], and therefore will fail the type
// checks below for `whereType<File>` and `whereType<Directory>`. This
// ensures that we are not returning project roots that are outside of
// [_ideWorkspaceRoots].
final directoryContents = await (dir.list(followLinks: false)).toList();
final pubspec = directoryContents
.whereType<File>()
.firstWhereOrNull((entity) => entity.path.endsWith('pubspec.yaml'));
if (pubspec != null) {
projectRoots.add(dir.uri);
}
final nextLevel = currentDepth + 1;
if (nextLevel < searchDepth) {
await Future.wait([
for (final dir in directoryContents.whereType<Directory>())
findProjectRoots(dir, currentDepth: nextLevel),
]);
}
}
}
await Future.wait([
for (final workspaceRoot in _ideWorkspaceRoots)
findProjectRoots(Directory.fromUri(workspaceRoot), currentDepth: 0),
]);
return UriList(uris: projectRoots).toJson();
}
Future<Map<String, Object?>> _readFileAsString(Parameters parameters) async {
final uri = _extractUri(parameters);
_ensureIDEWorkspaceRootsContainUri(uri);
final file = File.fromUri(uri);
if (!(await file.exists())) {
throw RpcErrorCodes.buildRpcException(
RpcErrorCodes.kFileDoesNotExist,
);
}
final content = await file.readAsString();
return FileContent(content: content).toJson();
}
Uri _extractUri(Parameters parameters) {
final uriString = parameters['uri'].asString;
final uri = Uri.parse(path.normalize(uriString));
if (uri.scheme != 'file') {
throw RpcErrorCodes.buildRpcException(
RpcErrorCodes.kExpectsUriParamWithFileScheme,
);
}
return uri;
}
Future<Map<String, Object?>> _writeFileAsString(Parameters parameters) async {
final uri = _extractUri(parameters);
final contents = parameters[DtdParameters.contents].asString;
final encoding = Encoding.getByName(
parameters[DtdParameters.encoding].asString,
)!;
_ensureIDEWorkspaceRootsContainUri(uri);
final file = File.fromUri(uri);
if (!(await file.exists())) {
await file.create(recursive: true);
}
await file.writeAsString(
contents,
encoding: encoding,
);
return RPCResponses.success;
}
Future<Map<String, Object?>> _listDirectoryContents(
Parameters parameters,
) async {
final uri = _extractUri(parameters);
_ensureIDEWorkspaceRootsContainUri(uri);
final dir = Directory.fromUri(uri);
if (!(await dir.exists())) {
throw RpcErrorCodes.buildRpcException(
RpcErrorCodes.kDirectoryDoesNotExist,
data: {'directory': dir.uri.toFilePath()},
);
}
final response = await (dir.list()).toList();
final uris = response.map((e) => e.uri).toList();
return UriList(uris: uris).toJson();
}
}