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