| // Copyright (c) 2014, 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:io'; | |
| import 'package:package_config/package_config.dart'; | |
| import 'package:path/path.dart' as p; | |
| /// [Resolver] resolves imports with respect to a given environment. | |
| class Resolver { | |
| @Deprecated('Use Resolver.create') | |
| Resolver({this.packagesPath, this.sdkRoot}) | |
| : _packages = packagesPath != null ? _parsePackages(packagesPath) : null, | |
| packagePath = null; | |
| Resolver._({ | |
| this.packagesPath, | |
| this.packagePath, | |
| this.sdkRoot, | |
| Map<String, Uri>? packages, | |
| }) : _packages = packages; | |
| static Future<Resolver> create({ | |
| String? packagesPath, | |
| String? packagePath, | |
| String? sdkRoot, | |
| }) async { | |
| return Resolver._( | |
| packagesPath: packagesPath, | |
| packagePath: packagePath, | |
| sdkRoot: sdkRoot, | |
| packages: packagesPath != null | |
| ? _parsePackages(packagesPath) | |
| : (packagePath != null ? await _parsePackage(packagePath) : null), | |
| ); | |
| } | |
| final String? packagesPath; | |
| final String? packagePath; | |
| final String? sdkRoot; | |
| final List<String> failed = []; | |
| final Map<String, Uri>? _packages; | |
| /// Returns the absolute path wrt. to the given environment or null, if the | |
| /// import could not be resolved. | |
| String? resolve(String scriptUri) { | |
| final uri = Uri.parse(scriptUri); | |
| if (uri.scheme == 'dart') { | |
| final sdkRoot = this.sdkRoot; | |
| if (sdkRoot == null) { | |
| // No sdk-root given, do not resolve dart: URIs. | |
| return null; | |
| } | |
| String filePath; | |
| if (uri.pathSegments.length > 1) { | |
| var path = uri.pathSegments[0]; | |
| // Drop patch files, since we don't have their source in the compiled | |
| // SDK. | |
| if (path.endsWith('-patch')) { | |
| failed.add('$uri'); | |
| return null; | |
| } | |
| // Canonicalize path. For instance: _collection-dev => _collection_dev. | |
| path = path.replaceAll('-', '_'); | |
| final pathSegments = [sdkRoot, path, ...uri.pathSegments.sublist(1)]; | |
| filePath = p.joinAll(pathSegments); | |
| } else { | |
| // Resolve 'dart:something' to be something/something.dart in the SDK. | |
| final lib = uri.path; | |
| filePath = p.join(sdkRoot, lib, '$lib.dart'); | |
| } | |
| return resolveSymbolicLinks(filePath); | |
| } | |
| if (uri.scheme == 'package') { | |
| final packages = _packages; | |
| if (packages == null) { | |
| return null; | |
| } | |
| final packageName = uri.pathSegments[0]; | |
| final packageUri = packages[packageName]; | |
| if (packageUri == null) { | |
| failed.add('$uri'); | |
| return null; | |
| } | |
| final packagePath = p.fromUri(packageUri); | |
| final pathInPackage = p.joinAll(uri.pathSegments.sublist(1)); | |
| return resolveSymbolicLinks(p.join(packagePath, pathInPackage)); | |
| } | |
| if (uri.scheme == 'file') { | |
| final resolved = resolveSymbolicLinks(p.fromUri(uri)); | |
| if (resolved == null || !_isWithinKnownRoots(resolved)) return null; | |
| return resolved; | |
| } | |
| // We cannot deal with anything else. | |
| failed.add('$uri'); | |
| return null; | |
| } | |
| /// Returns a canonicalized path, or `null` if the path cannot be resolved. | |
| String? resolveSymbolicLinks(String path) { | |
| final normalizedPath = p.normalize(path); | |
| final type = FileSystemEntity.typeSync(normalizedPath, followLinks: true); | |
| if (type == FileSystemEntityType.notFound) return null; | |
| return File(normalizedPath).resolveSymbolicLinksSync(); | |
| } | |
| /// The directories a `file:` URI is allowed to resolve into. | |
| /// | |
| /// `source` entries in coverage data are supplied by whatever produced the | |
| /// coverage JSON, which this library treats as untrusted input: nothing | |
| /// stops a crafted `file://` URI from pointing anywhere on disk, and | |
| /// callers (`format_coverage --pretty-print`, `filterIgnored`) read the | |
| /// resolved path's contents into their output. Without this check, a | |
| /// coverage.json with `"source": "file:///etc/passwd"` gets that file's | |
| /// contents printed straight into the coverage report. | |
| List<String>? _knownRoots; | |
| bool _isWithinKnownRoots(String path) { | |
| final roots = _knownRoots ??= [ | |
| ?packagePath, | |
| ?sdkRoot, | |
| ...?_packages?.values.map(p.fromUri), | |
| Directory.current.path, | |
| ].map(p.normalize).toList(); | |
| final normalized = p.normalize(path); | |
| return roots.any( | |
| (root) => normalized == root || p.isWithin(root, normalized), | |
| ); | |
| } | |
| static Map<String, Uri> _parsePackages(String packagesPath) { | |
| final content = File(packagesPath).readAsStringSync(); | |
| final packagesUri = p.toUri(packagesPath); | |
| final parsed = PackageConfig.parseString( | |
| content, | |
| Uri.base.resolveUri(packagesUri), | |
| ); | |
| return { | |
| for (var package in parsed.packages) package.name: package.packageUriRoot, | |
| }; | |
| } | |
| static Future<Map<String, Uri>?> _parsePackage(String packagePath) async { | |
| final parsed = await findPackageConfig(Directory(packagePath)); | |
| if (parsed == null) return null; | |
| return { | |
| for (var package in parsed.packages) package.name: package.packageUriRoot, | |
| }; | |
| } | |
| } | |
| /// Bazel URI resolver. | |
| class BazelResolver extends Resolver { | |
| /// Creates a Bazel resolver with the specified workspace path, if any. | |
| BazelResolver({this.workspacePath = ''}); | |
| final String workspacePath; | |
| /// Returns the absolute path wrt. to the given environment or null, if the | |
| /// import could not be resolved. | |
| @override | |
| String? resolve(String scriptUri) { | |
| final uri = Uri.parse(scriptUri); | |
| if (uri.scheme == 'dart') { | |
| // Ignore the SDK | |
| return null; | |
| } | |
| if (uri.scheme == 'package') { | |
| // TODO(cbracken) belongs in a Bazel package | |
| return _resolveBazelPackage(uri.pathSegments); | |
| } | |
| if (uri.scheme == 'file') { | |
| final runfilesPathSegment = '.runfiles/$workspacePath'.replaceAll( | |
| RegExp(r'/*$'), | |
| '/', | |
| ); | |
| final runfilesPos = uri.path.indexOf(runfilesPathSegment); | |
| if (runfilesPos >= 0) { | |
| final pathStart = runfilesPos + runfilesPathSegment.length; | |
| return uri.path.substring(pathStart); | |
| } | |
| return null; | |
| } | |
| if (uri.scheme == 'https' || uri.scheme == 'http') { | |
| return _extractHttpPath(uri); | |
| } | |
| // We cannot deal with anything else. | |
| failed.add('$uri'); | |
| return null; | |
| } | |
| String _extractHttpPath(Uri uri) { | |
| final packagesPos = uri.pathSegments.indexOf('packages'); | |
| if (packagesPos >= 0) { | |
| final workspacePath = uri.pathSegments.sublist(packagesPos + 1); | |
| return _resolveBazelPackage(workspacePath); | |
| } | |
| return uri.pathSegments.join('/'); | |
| } | |
| String _resolveBazelPackage(List<String> pathSegments) { | |
| // TODO(cbracken) belongs in a Bazel package | |
| final packageName = pathSegments[0]; | |
| final pathInPackage = pathSegments.sublist(1).join('/'); | |
| final packagePath = packageName.contains('.') | |
| ? packageName.replaceAll('.', '/') | |
| : 'third_party/dart/$packageName'; | |
| return '$packagePath/lib/$pathInPackage'; | |
| } | |
| } | |
| /// Loads the lines of imported resources. | |
| class Loader { | |
| final List<String> failed = []; | |
| /// Loads an imported resource and returns a [Future] with a [List] of lines. | |
| /// Returns `null` if the resource could not be loaded. | |
| Future<List<String>?> load(String path) async { | |
| try { | |
| // Ensure `readAsLines` runs within the try block so errors are caught. | |
| return await File(path).readAsLines(); | |
| } catch (_) { | |
| failed.add(path); | |
| return null; | |
| } | |
| } | |
| /// Loads an imported resource and returns a [List] of lines. | |
| /// Returns `null` if the resource could not be loaded. | |
| List<String>? loadSync(String path) { | |
| try { | |
| // Ensure `readAsLinesSync` runs within the try block so errors are | |
| // caught. | |
| return File(path).readAsLinesSync(); | |
| } catch (_) { | |
| failed.add(path); | |
| return null; | |
| } | |
| } | |
| } |