@@ -153,71 +153,24 @@ Future<CallToolResult> runCommandInRoot(
153153 required Sdk sdk,
154154}) async {
155155 rootConfig ?? = request.arguments;
156- final rootUriString = rootConfig? [ParameterNames .root] as String ? ;
157- if (rootUriString == null ) {
158- // This shouldn't happen based on the schema, but handle defensively.
159- return CallToolResult (
160- content: [
161- TextContent (text: 'Invalid root configuration: missing `root` key.' ),
162- ],
163- isError: true ,
164- )..failureReason ?? = CallToolFailureReason .noRootGiven;
165- }
166-
167- final root = knownRoots.firstWhereOrNull (
168- (root) => _isUnderRoot (root, rootUriString, fileSystem),
156+ final (
157+ root: Root ? root,
158+ paths: List <String >? paths,
159+ errorResult: CallToolResult ? errorResult,
160+ ) = validateRootConfig (
161+ rootConfig,
162+ defaultPaths: defaultPaths,
163+ fileSystem: fileSystem,
164+ knownRoots: knownRoots,
169165 );
170- if (root == null ) {
171- return CallToolResult (
172- content: [
173- TextContent (
174- text:
175- 'Invalid root $rootUriString , must be under one of the '
176- 'registered project roots:\n\n ${knownRoots .join ('\n ' )}' ,
177- ),
178- ],
179- isError: true ,
180- )..failureReason ?? = CallToolFailureReason .invalidRootPath;
181- }
182-
183- final rootUri = Uri .parse (rootUriString);
184- if (rootUri.scheme != 'file' ) {
185- return CallToolResult (
186- content: [
187- TextContent (
188- text:
189- 'Only file scheme uris are allowed for roots, but got '
190- '$rootUri ' ,
191- ),
192- ],
193- isError: true ,
194- )..failureReason ?? = CallToolFailureReason .invalidRootScheme;
195- }
196- final projectRoot = fileSystem.directory (rootUri);
166+ if (errorResult != null ) return errorResult;
197167
168+ final projectRoot = Uri .parse (root! .uri);
198169 final commandWithPaths = < String > [
199- await commandForRoot (rootUriString , fileSystem, sdk),
170+ await commandForRoot (root.uri , fileSystem, sdk),
200171 ...arguments,
201172 ];
202- final paths =
203- (rootConfig? [ParameterNames .paths] as List ? )? .cast <String >() ??
204- defaultPaths;
205- final invalidPaths = paths.where (
206- (path) => ! _isUnderRoot (root, path, fileSystem),
207- );
208- if (invalidPaths.isNotEmpty) {
209- return CallToolResult (
210- content: [
211- TextContent (
212- text:
213- 'Paths are not allowed to escape their project root:\n '
214- '${invalidPaths .join ('\n ' )}' ,
215- ),
216- ],
217- isError: true ,
218- )..failureReason ?? = CallToolFailureReason .invalidPath;
219- }
220- commandWithPaths.addAll (paths);
173+ if (paths != null ) commandWithPaths.addAll (paths);
221174
222175 final workingDir = fileSystem.directory (projectRoot.path);
223176 await workingDir.create (recursive: true );
@@ -252,6 +205,106 @@ Future<CallToolResult> runCommandInRoot(
252205 );
253206}
254207
208+ /// Validates a root argument given via [rootConfig] , ensuring that it falls
209+ /// under one of the [knownRoots] , and that all `paths` arguments are also under
210+ /// the given root.
211+ ///
212+ /// Returns a root on success, equal to the given root (but this could be a
213+ /// subdirectory of one of the [knownRoots] ), as well as any paths that were
214+ /// validated.
215+ ///
216+ /// If no [ParameterNames.paths] are provided, then the [defaultPaths] will be
217+ /// used, if present. Otherwise no paths are validated or will be returned.
218+ ///
219+ /// On failure, returns a [CallToolResult] .
220+ ({Root ? root, List <String >? paths, CallToolResult ? errorResult})
221+ validateRootConfig (
222+ Map <String , Object ?>? rootConfig, {
223+ List <String >? defaultPaths,
224+ required FileSystem fileSystem,
225+ required List <Root > knownRoots,
226+ }) {
227+ final rootUriString = rootConfig? [ParameterNames .root] as String ? ;
228+ if (rootUriString == null ) {
229+ // This shouldn't happen based on the schema, but handle defensively.
230+ return (
231+ root: null ,
232+ paths: null ,
233+ errorResult: CallToolResult (
234+ content: [
235+ TextContent (text: 'Invalid root configuration: missing `root` key.' ),
236+ ],
237+ isError: true ,
238+ )..failureReason ?? = CallToolFailureReason .noRootGiven,
239+ );
240+ }
241+
242+ final knownRoot = knownRoots.firstWhereOrNull (
243+ (root) => _isUnderRoot (root, rootUriString, fileSystem),
244+ );
245+ if (knownRoot == null ) {
246+ return (
247+ root: null ,
248+ paths: null ,
249+ errorResult: CallToolResult (
250+ content: [
251+ TextContent (
252+ text:
253+ 'Invalid root $rootUriString , must be under one of the '
254+ 'registered project roots:\n\n ${knownRoots .join ('\n ' )}' ,
255+ ),
256+ ],
257+ isError: true ,
258+ )..failureReason ?? = CallToolFailureReason .invalidRootPath,
259+ );
260+ }
261+ final root = Root (uri: rootUriString);
262+
263+ final rootUri = Uri .parse (rootUriString);
264+ if (rootUri.scheme != 'file' ) {
265+ return (
266+ root: null ,
267+ paths: null ,
268+ errorResult: CallToolResult (
269+ content: [
270+ TextContent (
271+ text:
272+ 'Only file scheme uris are allowed for roots, but got '
273+ '$rootUri ' ,
274+ ),
275+ ],
276+ isError: true ,
277+ )..failureReason ?? = CallToolFailureReason .invalidRootScheme,
278+ );
279+ }
280+
281+ final paths =
282+ (rootConfig? [ParameterNames .paths] as List ? )? .cast <String >() ??
283+ defaultPaths;
284+ if (paths != null ) {
285+ final invalidPaths = paths.where (
286+ (path) => ! _isUnderRoot (root, path, fileSystem),
287+ );
288+ if (invalidPaths.isNotEmpty) {
289+ return (
290+ root: null ,
291+ paths: null ,
292+ errorResult: CallToolResult (
293+ content: [
294+ TextContent (
295+ text:
296+ 'Paths are not allowed to escape their project root:\n '
297+ '${invalidPaths .join ('\n ' )}' ,
298+ ),
299+ ],
300+ isError: true ,
301+ )..failureReason ?? = CallToolFailureReason .invalidPath,
302+ );
303+ }
304+ }
305+ return (root: root, paths: paths, errorResult: null );
306+ }
307+
255308/// Returns 'dart' or 'flutter' based on the pubspec contents.
256309///
257310/// Throws an [ArgumentError] if there is no pubspec.
@@ -316,7 +369,8 @@ final rootSchema = Schema.string(
316369 description:
317370 'This must be equal to or a subdirectory of one of the roots '
318371 'allowed by the client. Must be a URI with a `file:` '
319- 'scheme (e.g. file:///absolute/path/to/root).' ,
372+ 'scheme (e.g. file:///absolute/path/to/root). Dart and Flutter project '
373+ 'roots can be identified by the existence of a `pubspec.yaml` file.' ,
320374);
321375
322376/// Very thin extension type for a pubspec just containing what we need.
0 commit comments