| // Copyright (c) 2012, 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. |
| |
| library builtin; |
| |
| // NOTE: Do not import 'dart:io' in builtin. |
| import 'dart:async'; |
| import 'dart:collection' hide LinkedList, LinkedListEntry; |
| import 'dart:_internal' hide Symbol; |
| import 'dart:io'; |
| import 'dart:convert'; |
| import 'dart:isolate'; |
| import 'dart:typed_data'; |
| |
| // Embedder sets this to true if the --trace-loading flag was passed on the |
| // command line. |
| bool _traceLoading = false; |
| |
| // Before handling an embedder entrypoint we finalize the setup of the |
| // dart:_builtin library. |
| bool _setupCompleted = false; |
| |
| // 'print' implementation. |
| // The standalone embedder registers the closurized _print function with the |
| // dart:core library. |
| void _printString(String s) native "Builtin_PrintString"; |
| |
| void _print(arg) { |
| _printString(arg.toString()); |
| } |
| |
| @pragma("vm:entry-point") |
| _getPrintClosure() => _print; |
| |
| // The current working directory when the embedder was launched. |
| late Uri _workingDirectory; |
| |
| // The URI that the root script was loaded from. Remembered so that |
| // package imports can be resolved relative to it. The root script is the basis |
| // for the root library in the VM. |
| Uri? _rootScript; |
| |
| // packagesConfig specified for the isolate. |
| Uri? _packagesConfigUri; |
| |
| // Packages are either resolved looking up in a map or resolved from within a |
| // package root. |
| bool get _packagesReady => (_packageMap != null) || (_packageError != null); |
| |
| // Error string set if there was an error resolving package configuration. |
| // For example not finding a .packages file or packages/ directory, malformed |
| // .packages file or any other related error. |
| String? _packageError = null; |
| |
| // The map describing how certain package names are mapped to Uris. |
| Uri? _packageConfig = null; |
| Map<String, Uri>? _packageMap = null; |
| |
| // Special handling for Windows paths so that they are compatible with URI |
| // handling. |
| // Embedder sets this to true if we are running on Windows. |
| @pragma("vm:entry-point") |
| bool _isWindows = false; |
| |
| // Logging from builtin.dart is prefixed with a '*'. |
| String _logId = (Isolate.current.hashCode % 0x100000).toRadixString(16); |
| _log(msg) { |
| _print("* $_logId $msg"); |
| } |
| |
| _sanitizeWindowsPath(path) { |
| // For Windows we need to massage the paths a bit according to |
| // http://blogs.msdn.com/b/ie/archive/2006/12/06/file-uris-in-windows.aspx |
| // |
| // Convert |
| // C:\one\two\three |
| // to |
| // /C:/one/two/three |
| |
| if (_isWindows == false) { |
| // Do nothing when not running Windows. |
| return path; |
| } |
| |
| var fixedPath = "${path.replaceAll('\\', '/')}"; |
| |
| if ((path.length > 2) && (path[1] == ':')) { |
| // Path begins with a drive letter. |
| return '/$fixedPath'; |
| } |
| |
| return fixedPath; |
| } |
| |
| _setPackagesConfig(String packagesParam) { |
| var packagesName = _sanitizeWindowsPath(packagesParam); |
| var packagesUri = Uri.parse(packagesName); |
| if (packagesUri.scheme == '') { |
| // Script does not have a scheme, assume that it is a path, |
| // resolve it against the working directory. |
| packagesUri = _workingDirectory.resolveUri(packagesUri); |
| } |
| _packagesConfigUri = packagesUri; |
| } |
| |
| // Given a uri with a 'package' scheme, return a Uri that is prefixed with |
| // the package root or resolved relative to the package configuration. |
| Uri _resolvePackageUri(Uri uri) { |
| assert(uri.scheme == "package"); |
| assert(_packagesReady); |
| |
| if (uri.host.isNotEmpty) { |
| var path = '${uri.host}${uri.path}'; |
| var right = 'package:$path'; |
| var wrong = 'package://$path'; |
| |
| throw "URIs using the 'package:' scheme should look like " |
| "'$right', not '$wrong'."; |
| } |
| |
| var packageNameEnd = uri.path.indexOf('/'); |
| if (packageNameEnd == 0) { |
| // Package URIs must have a non-empty package name (not start with "/"). |
| throw "URIS using the 'package:' scheme should look like " |
| "'package:packageName${uri.path}', not 'package:${uri.path}'"; |
| } |
| if (_traceLoading) { |
| _log('Resolving package with uri path: ${uri.path}'); |
| } |
| var resolvedUri; |
| final error = _packageError; |
| if (error != null) { |
| if (_traceLoading) { |
| _log("Resolving package with pending resolution error: $error"); |
| } |
| throw error; |
| } else { |
| if (packageNameEnd < 0) { |
| // Package URIs must have a path after the package name, even if it's |
| // just "/". |
| throw "URIS using the 'package:' scheme should look like " |
| "'package:${uri.path}/', not 'package:${uri.path}'"; |
| } |
| var packageName = uri.path.substring(0, packageNameEnd); |
| final mapping = _packageMap![packageName]; |
| if (_traceLoading) { |
| _log("Mapped '$packageName' package to '$mapping'"); |
| } |
| if (mapping == null) { |
| throw "No mapping for '$packageName' package when resolving '$uri'."; |
| } |
| var path; |
| assert(uri.path.length > packageName.length); |
| path = uri.path.substring(packageName.length + 1); |
| if (_traceLoading) { |
| _log("Path to be resolved in package: $path"); |
| } |
| resolvedUri = mapping.resolve(path); |
| } |
| if (_traceLoading) { |
| _log("Resolved '$uri' to '$resolvedUri'."); |
| } |
| return resolvedUri; |
| } |
| |
| void _requestPackagesMap(Uri? packageConfig) { |
| dynamic msg = null; |
| if (packageConfig != null) { |
| // Explicitly specified .packages path. |
| msg = _handlePackagesRequest(_traceLoading, -2, packageConfig); |
| } else { |
| // Search for .packages starting at the root script. |
| msg = _handlePackagesRequest(_traceLoading, -1, _rootScript!); |
| } |
| if (_traceLoading) { |
| _log("Requested packages map for '$_rootScript'."); |
| } |
| if (msg is String) { |
| if (_traceLoading) { |
| _log("Got failure response on package port: '$msg'"); |
| } |
| // Remember the error message. |
| _packageError = msg; |
| } else if (msg is List) { |
| // First entry contains the location of the loaded .packages file. |
| assert((msg.length % 2) == 0); |
| assert(msg.length >= 2); |
| assert(msg[1] == null); |
| _packageConfig = Uri.parse(msg[0]); |
| final pmap = new Map<String, Uri>(); |
| _packageMap = pmap; |
| for (var i = 2; i < msg.length; i += 2) { |
| // TODO(iposva): Complain about duplicate entries. |
| pmap[msg[i]] = Uri.parse(msg[i + 1]); |
| } |
| if (_traceLoading) { |
| _log("Setup package map: $_packageMap"); |
| } |
| } else { |
| _packageError = "Bad type of packages reply: ${msg.runtimeType}"; |
| if (_traceLoading) { |
| _log(_packageError); |
| } |
| } |
| } |
| |
| // The values go from ' ' to DEL and `x` means disallowed. |
| const String _invalidPackageNameChars = |
| 'x.xx.x.........x..........x.x.xx...........................xxxx.x..........................xxx.x'; |
| |
| bool _isValidPackageName(String packageName) { |
| const space = 0x20; |
| const del = 0x7F; |
| const dot = 0x2e; |
| const lowerX = 0x78; |
| for (int i = 0; i < packageName.length; ++i) { |
| final int char = packageName.codeUnitAt(i); |
| if (char < space || del < char) { |
| return false; |
| } |
| final int allowed = _invalidPackageNameChars.codeUnitAt(char - space); |
| assert(allowed == dot || allowed == lowerX); |
| if (allowed == lowerX) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| _parsePackagesFile(bool traceLoading, Uri packagesFile, String data) { |
| // The first entry contains the location of the identified .packages file |
| // instead of a mapping. |
| final List result = [packagesFile.toString(), null]; |
| |
| final lines = LineSplitter.split(data); |
| for (String line in lines) { |
| final hashIndex = line.indexOf('#'); |
| if (hashIndex == 0) { |
| continue; |
| } |
| if (hashIndex > 0) { |
| line = line.substring(0, hashIndex); |
| } |
| line = line.trimRight(); |
| if (line.isEmpty) { |
| continue; |
| } |
| |
| final colonIndex = line.indexOf(':'); |
| if (colonIndex <= 0) { |
| return 'Line in "$packagesFile" should be of the format ' |
| '`<package-name>:<path>" but was: "$line"'; |
| } |
| final packageName = line.substring(0, colonIndex); |
| if (!_isValidPackageName(packageName)) { |
| return 'Package name in $packagesFile contains disallowed characters (' |
| 'was: "$packageName")'; |
| } |
| |
| String packageUri = line.substring(colonIndex + 1); |
| if (traceLoading) { |
| _log("packageName: $packageName"); |
| _log("packageUri: $packageUri"); |
| } |
| // Ensure the package uri ends with a /. |
| if (!packageUri.endsWith('/')) { |
| packageUri += '/'; |
| } |
| final resolvedPackageUri = packagesFile.resolve(packageUri).toString(); |
| if (traceLoading) { |
| _log("mapping: $packageName -> $resolvedPackageUri"); |
| } |
| result.add(packageName); |
| result.add(resolvedPackageUri); |
| } |
| if (traceLoading) { |
| _log("Parsed packages file at $packagesFile. Sending:\n$result"); |
| } |
| return result; |
| } |
| |
| // The .dart_tool/package_config.json format is described in |
| // |
| // https://github.com/dart-lang/language/blob/master/accepted/future-releases/language-versioning/package-config-file-v2.md |
| // |
| // The returned list has the format: |
| // |
| // [0] Location of package_config.json file. |
| // [1] null |
| // [n*2] Name of n-th package |
| // [n*2 + 1] Location of n-th package's sources (as a String) |
| // |
| List _parsePackageConfig(bool traceLoading, Uri packageConfig, String data) { |
| final Map packageJson = json.decode(data); |
| final version = packageJson['configVersion']; |
| if (version != 2) { |
| throw 'The package configuration file has an unsupported version.'; |
| } |
| // The first entry contains the location of the identified |
| // .dart_tool/package_config.json file instead of a mapping. |
| final result = <dynamic>[packageConfig.toString(), null]; |
| final List packages = packageJson['packages'] ?? []; |
| for (final Map package in packages) { |
| String rootUri = package['rootUri']; |
| if (!rootUri.endsWith('/')) rootUri += '/'; |
| final String packageName = package['name']; |
| final String? packageUri = package['packageUri']; |
| final Uri resolvedRootUri = packageConfig.resolve(rootUri); |
| final Uri resolvedPackageUri = packageUri != null |
| ? resolvedRootUri.resolve(packageUri) |
| : resolvedRootUri; |
| if (packageUri != null && |
| !'$resolvedPackageUri'.contains('$resolvedRootUri')) { |
| throw 'The resolved "packageUri" is not a subdirectory of the "rootUri".'; |
| } |
| if (!_isValidPackageName(packageName)) { |
| throw 'Package name in $packageConfig contains disallowed characters (' |
| 'was: "$packageName")'; |
| } |
| result.add(packageName); |
| result.add(resolvedPackageUri.toString()); |
| if (traceLoading) { |
| _log('Resolved package "$packageName" to be at $resolvedPackageUri'); |
| } |
| } |
| return result; |
| } |
| |
| _findPackagesConfiguration(bool traceLoading, Uri base) { |
| try { |
| // Walk up the directory hierarchy to check for the existence of either one |
| // of |
| // - .packages (preferred) |
| // - .dart_tool/package_config.json |
| var currentDir = new File.fromUri(base).parent; |
| while (true) { |
| final dirUri = currentDir.uri; |
| |
| // We prefer using `.dart_tool/package_config.json` over `.packages`. |
| final packageConfig = dirUri.resolve(".dart_tool/package_config.json"); |
| if (traceLoading) { |
| _log("Checking for $packageConfig file."); |
| } |
| File file = File.fromUri(packageConfig); |
| bool exists = file.existsSync(); |
| if (traceLoading) { |
| _log("$packageConfig exists: $exists"); |
| } |
| if (exists) { |
| final data = utf8.decode(file.readAsBytesSync()); |
| if (traceLoading) { |
| _log("Loaded package config file from $packageConfig:$data\n"); |
| } |
| return _parsePackageConfig(traceLoading, packageConfig, data); |
| } |
| |
| // We fallback to using `.packages` if it exists. |
| final packagesFile = dirUri.resolve(".packages"); |
| if (traceLoading) { |
| _log("Checking for $packagesFile file."); |
| } |
| file = File.fromUri(packagesFile); |
| exists = file.existsSync(); |
| if (traceLoading) { |
| _log("$packagesFile exists: $exists"); |
| } |
| if (exists) { |
| final String data = utf8.decode(file.readAsBytesSync()); |
| if (traceLoading) { |
| _log("Loaded packages file from $packagesFile:\n$data"); |
| } |
| return _parsePackagesFile(traceLoading, packagesFile, data); |
| } |
| |
| final parentDir = currentDir.parent; |
| if (dirUri == parentDir.uri) break; |
| currentDir = parentDir; |
| } |
| |
| if (traceLoading) { |
| _log("Could not resolve a package configuration from $base"); |
| } |
| return "Could not resolve a package configuration for base at $base"; |
| } catch (e, s) { |
| if (traceLoading) { |
| _log("Error loading packages: $e\n$s"); |
| } |
| return "Uncaught error ($e) loading packages file."; |
| } |
| } |
| |
| int _indexOfFirstNonWhitespaceCharacter(String data) { |
| // Whitespace characters ignored in JSON spec: |
| // https://tools.ietf.org/html/rfc7159 |
| const tab = 0x09; |
| const lf = 0x0A; |
| const cr = 0x0D; |
| const space = 0x20; |
| |
| int index = 0; |
| while (index < data.length) { |
| final int char = data.codeUnitAt(index); |
| if (char != lf && char != cr && char != space && char != tab) { |
| break; |
| } |
| index++; |
| } |
| return index; |
| } |
| |
| bool _canBeValidJson(String data) { |
| const int openCurly = 0x7B; |
| final int index = _indexOfFirstNonWhitespaceCharacter(data); |
| return index < data.length && data.codeUnitAt(index) == openCurly; |
| } |
| |
| _parsePackageConfiguration(bool traceLoading, Uri resource, Uint8List bytes) { |
| try { |
| final data = utf8.decode(bytes); |
| if (_canBeValidJson(data)) { |
| return _parsePackageConfig(traceLoading, resource, data); |
| } else { |
| return _parsePackagesFile(traceLoading, resource, data); |
| } |
| } catch (e) { |
| return "The resource '$resource' is neither a valid '.packages' file nor " |
| "a valid '.dart_tool/package_config.json' file."; |
| } |
| } |
| |
| bool _isValidUtf8DataUrl(UriData data) { |
| final mime = data.mimeType; |
| if (mime != "text/plain") { |
| return false; |
| } |
| final charset = data.charset; |
| if (charset != "utf-8" && charset != "US-ASCII") { |
| return false; |
| } |
| return true; |
| } |
| |
| _handlePackagesRequest(bool traceLoading, int tag, Uri resource) { |
| try { |
| if (tag == -1) { |
| if (resource.scheme == '' || resource.scheme == 'file') { |
| return _findPackagesConfiguration(traceLoading, resource); |
| } else { |
| return "Unsupported scheme used to locate .packages file:'$resource'."; |
| } |
| } else if (tag == -2) { |
| if (traceLoading) { |
| _log("Handling load of packages map: '$resource'."); |
| } |
| late Uint8List bytes; |
| if (resource.scheme == '' || resource.scheme == 'file') { |
| final file = File.fromUri(resource); |
| if (!file.existsSync()) { |
| return "Packages file '$resource' does not exit."; |
| } |
| bytes = file.readAsBytesSync(); |
| } else if (resource.scheme == 'data') { |
| final uriData = resource.data!; |
| if (!_isValidUtf8DataUrl(uriData)) { |
| return "The data resource '$resource' must have a 'text/plain' mime " |
| "type and a 'utf-8' or 'US-ASCII' charset."; |
| } |
| bytes = uriData.contentAsBytes(); |
| } else { |
| return "Unknown scheme (${resource.scheme}) for package file at " |
| "'$resource'."; |
| } |
| return _parsePackageConfiguration(traceLoading, resource, bytes); |
| } else { |
| return "Unknown packages request tag: $tag for '$resource'."; |
| } |
| } catch (e, s) { |
| if (traceLoading) { |
| _log("Error handling packages request: $e\n$s"); |
| } |
| return "Uncaught error ($e) handling packages request."; |
| } |
| } |
| |
| // Embedder Entrypoint: |
| // The embedder calls this method to initial the package resolution state. |
| @pragma("vm:entry-point") |
| void _Init( |
| String? packagesConfig, String workingDirectory, String? rootScript) { |
| // Register callbacks and hooks with the rest of core libraries. |
| _setupHooks(); |
| |
| // _workingDirectory must be set first. |
| _workingDirectory = new Uri.directory(workingDirectory); |
| |
| // setup _rootScript. |
| if (rootScript != null) { |
| _rootScript = Uri.parse(rootScript); |
| } |
| |
| // If the --packages flag was passed, setup _packagesConfig. |
| if (packagesConfig != null) { |
| _packageMap = null; |
| _setPackagesConfig(packagesConfig); |
| } |
| } |
| |
| // Embedder Entrypoint: |
| // The embedder calls this method with the current working directory. |
| @pragma("vm:entry-point") |
| void _setWorkingDirectory(String cwd) { |
| if (!_setupCompleted) { |
| _setupHooks(); |
| } |
| if (_traceLoading) { |
| _log('Setting working directory: $cwd'); |
| } |
| _workingDirectory = new Uri.directory(cwd); |
| if (_traceLoading) { |
| _log('Working directory URI: $_workingDirectory'); |
| } |
| } |
| |
| // Embedder Entrypoint: |
| // The embedder calls this method with the value of the --packages command line |
| // option. It can point to a ".packages" or a ".dart_tool/package_config.json" |
| // file. |
| @pragma("vm:entry-point") |
| String _setPackagesMap(String packagesParam) { |
| if (!_setupCompleted) { |
| _setupHooks(); |
| } |
| // First convert the packages parameter from the command line to a URI which |
| // can be handled by the loader code. |
| // TODO(iposva): Consider refactoring the common code below which is almost |
| // shared with resolution of the root script. |
| if (_traceLoading) { |
| _log("Resolving packages map: $packagesParam"); |
| } |
| if (_workingDirectory == null) { |
| throw 'No current working directory set.'; |
| } |
| var packagesName = _sanitizeWindowsPath(packagesParam); |
| var packagesUri = Uri.parse(packagesName); |
| if (packagesUri.scheme == '') { |
| // Script does not have a scheme, assume that it is a path, |
| // resolve it against the working directory. |
| packagesUri = _workingDirectory.resolveUri(packagesUri); |
| } |
| var packagesUriStr = packagesUri.toString(); |
| VMLibraryHooks.packageConfigString = packagesUriStr; |
| if (_traceLoading) { |
| _log('Resolved packages map to: $packagesUri'); |
| } |
| return packagesUriStr; |
| } |
| |
| // Resolves the script uri in the current working directory iff the given uri |
| // did not specify a scheme (e.g. a path to a script file on the command line). |
| @pragma("vm:entry-point") |
| String _resolveScriptUri(String scriptName) { |
| if (_traceLoading) { |
| _log("Resolving script: $scriptName"); |
| } |
| if (_workingDirectory == null) { |
| throw 'No current working directory set.'; |
| } |
| scriptName = _sanitizeWindowsPath(scriptName); |
| |
| var scriptUri = Uri.parse(scriptName); |
| if (scriptUri.scheme == '') { |
| // Script does not have a scheme, assume that it is a path, |
| // resolve it against the working directory. |
| scriptUri = _workingDirectory.resolveUri(scriptUri); |
| } |
| |
| // Remember the root script URI so that we can resolve packages based on |
| // this location. |
| _rootScript = scriptUri; |
| |
| if (_traceLoading) { |
| _log('Resolved entry point to: $_rootScript'); |
| } |
| return scriptUri.toString(); |
| } |
| |
| // Register callbacks and hooks with the rest of the core libraries. |
| @pragma("vm:entry-point") |
| _setupHooks() { |
| _setupCompleted = true; |
| VMLibraryHooks.packageConfigUriFuture = _getPackageConfigFuture; |
| VMLibraryHooks.resolvePackageUriFuture = _resolvePackageUriFuture; |
| } |
| |
| Future<Uri?> _getPackageConfigFuture() { |
| if (_traceLoading) { |
| _log("Request for package config from user code."); |
| } |
| if (!_packagesReady) { |
| _requestPackagesMap(_packagesConfigUri); |
| } |
| // Respond with the packages config (if any) after package resolution. |
| return Future.value(_packageConfig); |
| } |
| |
| Future<Uri?> _resolvePackageUriFuture(Uri packageUri) { |
| if (_traceLoading) { |
| _log("Request for package Uri resolution from user code: $packageUri"); |
| } |
| if (packageUri.scheme != "package") { |
| if (_traceLoading) { |
| _log("Non-package Uri, returning unmodified: $packageUri"); |
| } |
| // Return the incoming parameter if not passed a package: URI. |
| return Future.value(packageUri); |
| } |
| if (!_packagesReady) { |
| _requestPackagesMap(_packagesConfigUri); |
| } |
| Uri? resolvedUri; |
| try { |
| resolvedUri = _resolvePackageUri(packageUri); |
| } catch (e, s) { |
| if (_traceLoading) { |
| _log("Exception when resolving package URI: $packageUri:\n$e\n$s"); |
| } |
| resolvedUri = null; |
| } |
| if (_traceLoading) { |
| _log("Resolved '$packageUri' to '$resolvedUri'"); |
| } |
| return Future.value(resolvedUri); |
| } |