| // Copyright 2020 The Dart Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // @dart = 2.9 |
| |
| // Note: this is a copy from flutter tools, updated to work with dwds tests |
| |
| import 'dart:async'; |
| import 'dart:convert'; |
| import 'dart:io'; |
| import 'dart:typed_data'; |
| |
| import 'package:dwds/dwds.dart'; |
| import 'package:file/file.dart'; |
| import 'package:logging/logging.dart'; |
| import 'package:mime/mime.dart' as mime; |
| import 'package:package_config/package_config.dart'; // ignore: deprecated_member_use |
| import 'package:path/path.dart' as p; |
| import 'package:shelf/shelf.dart' as shelf; |
| |
| import 'utilities.dart'; |
| |
| Logger _logger = Logger('TestAssetServer'); |
| |
| class TestAssetServer implements AssetReader { |
| TestAssetServer( |
| this._root, |
| this._httpServer, |
| this._packageConfig, |
| this.internetAddress, |
| this._fileSystem, |
| ); |
| |
| // Fallback to "application/octet-stream" on null which |
| // makes no claims as to the structure of the data. |
| static const String _defaultMimeType = 'application/octet-stream'; |
| final FileSystem _fileSystem; |
| |
| /// Start the web asset server on a [hostname] and [port]. |
| /// |
| /// Unhandled exceptions will throw a exception with the error and stack |
| /// trace. |
| static Future<TestAssetServer> start( |
| FileSystem fileSystem, |
| String root, |
| String hostname, |
| int port, |
| UrlEncoder urlTunneller, |
| PackageConfig packageConfig, |
| ) async { |
| var address = (await InternetAddress.lookup(hostname)).first; |
| var httpServer = await HttpServer.bind(address, port); |
| var server = |
| TestAssetServer(root, httpServer, packageConfig, address, fileSystem); |
| return server; |
| } |
| |
| final String _root; |
| final HttpServer _httpServer; |
| final Map<String, Uint8List> _files = <String, Uint8List>{}; |
| final Map<String, Uint8List> _sourcemaps = <String, Uint8List>{}; |
| final Map<String, Uint8List> _metadata = <String, Uint8List>{}; |
| String _mergedMetadata; |
| final PackageConfig _packageConfig; |
| final InternetAddress internetAddress; |
| |
| Uint8List getFile(String path) => _files[path]; |
| |
| Uint8List getSourceMap(String path) => _sourcemaps[path]; |
| |
| // handle requests for JavaScript source, dart sources maps, or asset files. |
| Future<shelf.Response> handleRequest(shelf.Request request) async { |
| var headers = <String, String>{}; |
| |
| // Index file is serverd from the _root directory |
| if (request.url.path.endsWith('index.html')) { |
| final indexFile = _fileSystem.currentDirectory |
| .childDirectory(_root) |
| .childFile('index.html'); |
| if (indexFile.existsSync()) { |
| headers[HttpHeaders.contentTypeHeader] = 'text/html'; |
| headers[HttpHeaders.contentLengthHeader] = |
| indexFile.lengthSync().toString(); |
| return shelf.Response.ok(indexFile.openRead(), headers: headers); |
| } |
| return shelf.Response.notFound(''); |
| } |
| |
| // NOTE: shelf removes leading `/` for some reason. |
| var requestPath = request.url.path.startsWith('/') |
| ? request.url.path |
| : '/${request.url.path}'; |
| |
| // If this is a JavaScript file, it must be in the in-memory cache. |
| // Attempt to look up the file by URI. |
| if (_files.containsKey(requestPath)) { |
| final List<int> bytes = getFile(requestPath); |
| headers[HttpHeaders.contentLengthHeader] = bytes.length.toString(); |
| headers[HttpHeaders.contentTypeHeader] = 'application/javascript'; |
| return shelf.Response.ok(bytes, headers: headers); |
| } |
| // If this is a sourcemap file, then it might be in the in-memory cache. |
| // Attempt to lookup the file by URI. |
| if (_sourcemaps.containsKey(requestPath)) { |
| final List<int> bytes = getSourceMap(requestPath); |
| headers[HttpHeaders.contentLengthHeader] = bytes.length.toString(); |
| headers[HttpHeaders.contentTypeHeader] = 'application/json'; |
| return shelf.Response.ok(bytes, headers: headers); |
| } |
| |
| var file = _resolveDartFile(requestPath); |
| if (!file.existsSync()) { |
| return shelf.Response.notFound(''); |
| } |
| |
| var length = file.lengthSync(); |
| // Attempt to determine the file's mime type. if this is not provided some |
| // browsers will refuse to render images/show video et cetera. If the tool |
| // cannot determine a mime type, fall back to application/octet-stream. |
| String mimeType; |
| if (length >= 12) { |
| mimeType = mime.lookupMimeType( |
| file.path, |
| headerBytes: await file.openRead(0, 12).first, |
| ); |
| } |
| mimeType ??= _defaultMimeType; |
| headers[HttpHeaders.contentLengthHeader] = length.toString(); |
| headers[HttpHeaders.contentTypeHeader] = mimeType; |
| return shelf.Response.ok(file.openRead(), headers: headers); |
| } |
| |
| /// Tear down the http server running. |
| @override |
| Future<void> close() { |
| return _httpServer.close(); |
| } |
| |
| /// Write a single file into the in-memory cache. |
| void writeFile(String filePath, String contents) { |
| _files[filePath] = Uint8List.fromList(utf8.encode(contents)); |
| } |
| |
| /// Update the in-memory asset server with the provided source and manifest files. |
| /// |
| /// Returns a list of updated modules. |
| List<String> write( |
| File codeFile, File manifestFile, File sourcemapFile, File metadataFile) { |
| var modules = <String>[]; |
| var codeBytes = codeFile.readAsBytesSync(); |
| var sourcemapBytes = sourcemapFile.readAsBytesSync(); |
| var metadataBytes = metadataFile.readAsBytesSync(); |
| var manifest = |
| castStringKeyedMap(json.decode(manifestFile.readAsStringSync())); |
| for (var filePath in manifest.keys) { |
| if (filePath == null) { |
| _logger.severe('Invalid manfiest file: $filePath'); |
| continue; |
| } |
| var offsets = castStringKeyedMap(manifest[filePath]); |
| var codeOffsets = (offsets['code'] as List<dynamic>).cast<int>(); |
| var sourcemapOffsets = |
| (offsets['sourcemap'] as List<dynamic>).cast<int>(); |
| var metadataOffsets = (offsets['metadata'] as List<dynamic>).cast<int>(); |
| if (codeOffsets.length != 2 || |
| sourcemapOffsets.length != 2 || |
| metadataOffsets.length != 2) { |
| _logger.severe('Invalid manifest byte offsets: $offsets'); |
| continue; |
| } |
| |
| var codeStart = codeOffsets[0]; |
| var codeEnd = codeOffsets[1]; |
| if (codeStart < 0 || codeEnd > codeBytes.lengthInBytes) { |
| _logger.severe('Invalid byte index: [$codeStart, $codeEnd]'); |
| continue; |
| } |
| var byteView = Uint8List.view( |
| codeBytes.buffer, |
| codeStart, |
| codeEnd - codeStart, |
| ); |
| _files[filePath] = byteView; |
| |
| var sourcemapStart = sourcemapOffsets[0]; |
| var sourcemapEnd = sourcemapOffsets[1]; |
| if (sourcemapStart < 0 || sourcemapEnd > sourcemapBytes.lengthInBytes) { |
| _logger.severe('Invalid byte index: [$sourcemapStart, $sourcemapEnd]'); |
| continue; |
| } |
| var sourcemapView = Uint8List.view( |
| sourcemapBytes.buffer, |
| sourcemapStart, |
| sourcemapEnd - sourcemapStart, |
| ); |
| _sourcemaps['$filePath.map'] = sourcemapView; |
| |
| var metadataStart = metadataOffsets[0]; |
| var metadataEnd = metadataOffsets[1]; |
| if (metadataStart < 0 || metadataEnd > metadataBytes.lengthInBytes) { |
| _logger.severe('Invalid byte index: [$metadataStart, $metadataEnd]'); |
| continue; |
| } |
| var metadataView = Uint8List.view( |
| metadataBytes.buffer, |
| metadataStart, |
| metadataEnd - metadataStart, |
| ); |
| _metadata['$filePath.metadata'] = metadataView; |
| |
| modules.add(filePath); |
| } |
| |
| _mergedMetadata = _metadata.values |
| .map((Uint8List encoded) => utf8.decode(encoded)) |
| .join('\n'); |
| |
| return modules; |
| } |
| |
| // Attempt to resolve `path` to a dart file. |
| File _resolveDartFile(String path) { |
| // If this is a dart file, it must be on the local file system and is |
| // likely coming from a source map request. The tool doesn't currently |
| // consider the case of Dart files as assets. |
| var dartFile = |
| _fileSystem.file(_fileSystem.currentDirectory.uri.resolve(path)); |
| if (dartFile.existsSync()) { |
| return dartFile; |
| } |
| |
| var segments = p.split(path); |
| if (segments.first.isEmpty) { |
| segments.removeAt(0); |
| } |
| |
| // The file might have been a package file which is signaled by a |
| // `/packages/<package>/<path>` request. |
| if (segments.first == 'packages') { |
| var packageFile = _fileSystem.file(_packageConfig |
| .resolve(Uri(scheme: 'package', pathSegments: segments.skip(1)))); |
| if (packageFile.existsSync()) { |
| return packageFile; |
| } |
| } |
| |
| // Otherwise it must be a Dart SDK source. |
| var dartSdkParent = _fileSystem.directory(dartSdkPath).parent; |
| var dartSdkFile = _fileSystem.file( |
| _fileSystem.path.joinAll(<String>[dartSdkParent.path, ...segments])); |
| return dartSdkFile; |
| } |
| |
| @override |
| Future<String> dartSourceContents(String serverPath) { |
| var result = _resolveDartFile(serverPath); |
| if (result.existsSync()) { |
| return result.readAsString(); |
| } |
| return null; |
| } |
| |
| @override |
| Future<String> sourceMapContents(String serverPath) async { |
| var path = '/$serverPath'; |
| if (_sourcemaps.containsKey(path)) { |
| return utf8.decode(_sourcemaps[path]); |
| } |
| return null; |
| } |
| |
| @override |
| Future<String> metadataContents(String serverPath) async { |
| if (serverPath.endsWith('.ddc_merged_metadata')) { |
| return _mergedMetadata; |
| } |
| var path = '/$serverPath'; |
| if (_metadata.containsKey(path)) { |
| return utf8.decode(_metadata[path]); |
| } |
| return null; |
| } |
| } |
| |
| /// Given a data structure which is a Map of String to dynamic values, return |
| /// the same structure (`Map<String, dynamic>`) with the correct runtime types. |
| Map<String, dynamic> castStringKeyedMap(dynamic untyped) { |
| var map = untyped as Map<dynamic, dynamic>; |
| return map?.cast<String, dynamic>(); |
| } |