blob: 27c652b6ead2441d99f0509dfc1e317d9d49398d [file] [log] [blame]
// Copyright (c) 2018, 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.
import 'dart:async';
import 'dart:convert';
import 'package:analysis_server/src/utilities/stream.dart';
import 'package:collection/collection.dart';
class InvalidEncodingError {
final String headers;
InvalidEncodingError(this.headers);
@override
String toString() =>
'Encoding in supplied headers is not supported.\n\nHeaders:\n$headers';
}
class LspHeaders {
final String rawHeaders;
final int contentLength;
final String? encoding;
LspHeaders(this.rawHeaders, this.contentLength, this.encoding);
}
/// Transforms a stream of LSP data in the form:
///
/// Content-Length: xxx\r\n
/// Content-Type: application/vscode-jsonrpc; charset=utf-8\r\n
/// \r\n
/// { JSON payload }
///
/// into just the JSON payload, decoded with the specified encoding. Line endings
/// for headers must be \r\n on all platforms as defined in the LSP spec.
class LspPacketTransformer extends StreamTransformerBase<List<int>, String> {
@override
Stream<String> bind(Stream<List<int>> stream) {
LspHeaders? headersState;
final buffer = <int>[];
var controller = MoreTypedStreamController<String,
_LspPacketTransformerListenData, _LspPacketTransformerPauseData>(
onListen: (controller) {
var input = stream.expand((b) => b).listen(
(codeUnit) {
buffer.add(codeUnit);
var headers = headersState;
if (headers == null && _endsWithCrLfCrLf(buffer)) {
headersState = _parseHeaders(buffer);
buffer.clear();
} else if (headers != null &&
buffer.length >= headers.contentLength) {
// UTF-8 is the default - and only supported - encoding for LSP.
// The string 'utf8' is valid since it was published in the original spec.
// Any other encodings should be rejected with an error.
if ([null, 'utf-8', 'utf8']
.contains(headers.encoding?.toLowerCase())) {
controller.add(utf8.decode(buffer));
} else {
controller.addError(InvalidEncodingError(headers.rawHeaders));
}
buffer.clear();
headersState = null;
}
},
onError: controller.addError,
onDone: controller.close,
);
return _LspPacketTransformerListenData(input);
},
onPause: (listenData) {
listenData.input.pause();
return _LspPacketTransformerPauseData();
},
onResume: (listenData, pauseData) => listenData.input.resume(),
onCancel: (listenData) => listenData.input.cancel(),
);
return controller.controller.stream;
}
/// Whether [buffer] ends in '\r\n\r\n'.
static bool _endsWithCrLfCrLf(List<int> buffer) {
var l = buffer.length;
return l > 4 &&
buffer[l - 1] == 10 &&
buffer[l - 2] == 13 &&
buffer[l - 3] == 10 &&
buffer[l - 4] == 13;
}
static String? _extractEncoding(String? header) {
final charset = header
?.split(';')
.map((s) => s.trim().toLowerCase())
.firstWhereOrNull((s) => s.startsWith('charset='));
return charset?.split('=')[1];
}
/// Decodes [buffer] into a String and returns the 'Content-Length' header value.
static LspHeaders _parseHeaders(List<int> buffer) {
// Headers are specified as always ASCII in LSP.
final asString = ascii.decode(buffer);
final headers = asString.split('\r\n');
final lengthHeader =
headers.firstWhere((h) => h.startsWith('Content-Length'));
final length = lengthHeader.split(':').last.trim();
final contentTypeHeader =
headers.firstWhereOrNull((h) => h.startsWith('Content-Type'));
final encoding = _extractEncoding(contentTypeHeader);
return LspHeaders(asString, int.parse(length), encoding);
}
}
/// The data class for [StreamController.onListen].
class _LspPacketTransformerListenData {
final StreamSubscription<int> input;
_LspPacketTransformerListenData(this.input);
}
/// The marker class for [StreamController.onPause].
class _LspPacketTransformerPauseData {}