blob: b63b6e05ea2505358ebb0f65aafa272de7b86fcc [file] [log] [blame] [edit]
// 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>();
}