| // 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(); |
| } |
| } |