| // 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. |
| |
| // 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/asset_reader.dart'; |
| import 'package:dwds/config.dart'; |
| import 'package:file/file.dart'; |
| import 'package:logging/logging.dart'; |
| import 'package:mime/mime.dart' as mime; |
| import 'package:shelf/shelf.dart' as shelf; |
| import 'package:test_common/test_sdk_layout.dart'; |
| |
| class TestAssetServer implements AssetReader { |
| late final String _basePath; |
| final String index; |
| |
| final _logger = Logger('TestAssetServer'); |
| |
| // 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; |
| final HttpServer _httpServer; |
| final Map<String, Uint8List> _files = {}; |
| final Map<String, Uint8List> _sourceMaps = {}; |
| final Map<String, Uint8List> _metadata = {}; |
| late String _mergedMetadata; |
| final PackageUriMapper _packageUriMapper; |
| final InternetAddress internetAddress; |
| final TestSdkLayout _sdkLayout; |
| |
| TestAssetServer( |
| this.index, |
| this._httpServer, |
| this._packageUriMapper, |
| this.internetAddress, |
| this._fileSystem, |
| this._sdkLayout, |
| ) { |
| _basePath = _parseBasePathFromIndexHtml(index); |
| } |
| |
| @override |
| String get basePath => _basePath; |
| |
| bool hasFile(String path) => _files.containsKey(path); |
| Uint8List getFile(String path) => _files[path]!; |
| |
| bool hasSourceMap(String path) => _sourceMaps.containsKey(path); |
| Uint8List getSourceMap(String path) => _sourceMaps[path]!; |
| |
| bool hasMetadata(String path) => _metadata.containsKey(path); |
| Uint8List getMetadata(String path) => _metadata[path]!; |
| |
| /// 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( |
| String sdkDirectory, |
| FileSystem fileSystem, |
| String index, |
| String hostname, |
| int port, |
| UrlEncoder? urlTunneler, |
| PackageUriMapper packageUriMapper, |
| ) async { |
| final address = (await InternetAddress.lookup(hostname)).first; |
| final httpServer = await HttpServer.bind(address, port); |
| final sdkLayout = TestSdkLayout.createDefault(sdkDirectory); |
| final server = TestAssetServer( |
| index, httpServer, packageUriMapper, address, fileSystem, sdkLayout); |
| return server; |
| } |
| |
| // handle requests for JavaScript source, dart sources maps, or asset files. |
| Future<shelf.Response> handleRequest(shelf.Request request) async { |
| if (request.method != 'GET') { |
| // Assets are served via GET only. |
| return shelf.Response.notFound(''); |
| } |
| final requestPath = _stripBasePath(request.url.path, basePath); |
| if (requestPath == null) { |
| return shelf.Response.notFound(''); |
| } |
| |
| var headers = <String, String>{}; |
| |
| if (request.url.path.endsWith('.html')) { |
| var indexFile = _fileSystem.file(index); |
| 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(''); |
| } |
| |
| // If this is a JavaScript file, it must be in the in-memory cache. |
| // Attempt to look up the file by URI. |
| if (hasFile(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 (hasSourceMap(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); |
| } |
| // If this is a metadata file, then it might be in the in-memory cache. |
| // Attempt to lookup the file by URI. |
| if (hasMetadata(requestPath)) { |
| final List<int> bytes = getMetadata(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) { |
| 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, |
| ); |
| |
| final fileName = |
| filePath.startsWith('/') ? filePath.substring(1) : filePath; |
| _files[fileName] = 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['$fileName.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['$fileName.metadata'] = metadataView; |
| |
| modules.add(fileName); |
| } |
| |
| _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. |
| final dartFile = |
| _fileSystem.file(_fileSystem.currentDirectory.uri.resolve(path)); |
| if (dartFile.existsSync()) { |
| return dartFile; |
| } |
| |
| final segments = path.split('/'); |
| |
| // The file might have been a package file which is signaled by a |
| // `/packages/<package>/<path>` request. |
| if (segments.first == 'packages') { |
| final resolved = _packageUriMapper.serverPathToResolvedUri(path); |
| final packageFile = _fileSystem.file(resolved); |
| if (packageFile.existsSync()) { |
| return packageFile; |
| } |
| _logger.severe('Package file not found: $path ($packageFile)'); |
| } |
| |
| // Otherwise it must be a Dart SDK source. |
| var dartSdkParent = _fileSystem.directory(_sdkLayout.sdkDirectory).parent; |
| var dartSdkFile = _fileSystem.file( |
| _fileSystem.path.joinAll(<String>[dartSdkParent.path, ...segments])); |
| return dartSdkFile; |
| } |
| |
| @override |
| Future<String?> dartSourceContents(String serverPath) async { |
| final stripped = _stripBasePath(serverPath, basePath); |
| if (stripped != null) { |
| var result = _resolveDartFile(stripped); |
| if (result.existsSync()) { |
| return result.readAsString(); |
| } |
| } |
| _logger.severe('Source not found: $serverPath'); |
| return null; |
| } |
| |
| @override |
| Future<String?> sourceMapContents(String serverPath) async { |
| final stripped = _stripBasePath(serverPath, basePath); |
| if (stripped != null) { |
| if (hasSourceMap(stripped)) { |
| return utf8.decode(getSourceMap(stripped)); |
| } |
| } |
| _logger.severe('Source map not found: $serverPath'); |
| return null; |
| } |
| |
| @override |
| Future<String?> metadataContents(String serverPath) async { |
| final stripped = _stripBasePath(serverPath, basePath); |
| if (stripped != null) { |
| if (stripped.endsWith('.ddc_merged_metadata')) { |
| return _mergedMetadata; |
| } |
| if (hasMetadata(stripped)) { |
| return utf8.decode(getMetadata(stripped)); |
| } |
| } |
| _logger.severe('Metadata not found: $serverPath'); |
| return null; |
| } |
| |
| String _parseBasePathFromIndexHtml(String index) { |
| final file = _fileSystem.file(index); |
| if (!file.existsSync()) { |
| throw StateError('Index file $index is not found'); |
| } |
| final contents = file.readAsStringSync(); |
| final matches = RegExp(r'<base href="/([^>]*)/">').allMatches(contents); |
| if (matches.isEmpty) return ''; |
| return matches.first.group(1) ?? ''; |
| } |
| |
| String? _stripBasePath(String path, String basePath) { |
| path = stripLeadingSlashes(path); |
| if (path.startsWith(basePath)) { |
| path = path.substring(basePath.length); |
| } else { |
| // The given path isn't under base path, return null to indicate that. |
| _logger.severe('Path is not under $basePath: $path'); |
| return null; |
| } |
| return stripLeadingSlashes(path); |
| } |
| } |
| |
| /// 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>(); |
| } |