Skip to content

Commit 7c50837

Browse files
Martin LercherMartin Lercher
authored andcommitted
BugFix: content-length is counted in bytes so we can't use TextReaders and TextWriters. So: Stream-s.
1 parent 771ceff commit 7c50837

File tree

7 files changed

+81
-34
lines changed

7 files changed

+81
-34
lines changed

sample/SampleServer/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ static async Task MainAsync(string[] args)
2424
// await Task.Delay(100);
2525
//}
2626

27-
var server = new LanguageServer(Console.In, Console.Out);
27+
var server = new LanguageServer(Console.OpenStandardInput(), Console.OpenStandardOutput());
2828

2929
server.AddHandler(new TextDocumentHandler(server));
3030

src/JsonRpc/Connection.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public class Connection : IDisposable
1818
private readonly IRequestRouter _requestRouter;
1919

2020
public Connection(
21-
TextReader input,
21+
Stream input,
2222
IOutputHandler outputHandler,
2323
IReciever reciever,
2424
IRequestProcessIdentifier requestProcessIdentifier,

src/JsonRpc/InputHandler.cs

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public class InputHandler : IInputHandler
2121
public static char[] HeaderKeys = { CR, LF, ':' };
2222
public const short MinBuffer = 21; // Minimum size of the buffer "Content-Length: X\r\n\r\n"
2323

24-
private readonly TextReader _input;
24+
private readonly Stream _input;
2525
private readonly IOutputHandler _outputHandler;
2626
private readonly IReciever _reciever;
2727
private readonly IRequestProcessIdentifier _requestProcessIdentifier;
@@ -32,14 +32,15 @@ public class InputHandler : IInputHandler
3232
private Thread _queueThread;
3333

3434
public InputHandler(
35-
TextReader input,
35+
Stream input,
3636
IOutputHandler outputHandler,
3737
IReciever reciever,
3838
IRequestProcessIdentifier requestProcessIdentifier,
3939
IRequestRouter requestRouter,
4040
IResponseRouter responseRouter
4141
)
4242
{
43+
if (!input.CanRead) throw new ArgumentException($"must provide a readable stream for {nameof(input)}", nameof(input));
4344
_input = input;
4445
_outputHandler = outputHandler;
4546
_reciever = reciever;
@@ -54,7 +55,7 @@ IResponseRouter responseRouter
5455
}
5556

5657
internal InputHandler(
57-
TextReader input,
58+
Stream input,
5859
IOutputHandler outputHandler,
5960
IReciever reciever,
6061
IRequestProcessIdentifier requestProcessIdentifier,
@@ -75,19 +76,23 @@ public void Start()
7576

7677
private async void ProcessInputStream()
7778
{
79+
// header is encoded in ASCII
80+
// "Content-Length: 0" counts bytes for the following content
81+
// content is encoded in UTF-8
7882
while (true)
7983
{
8084
if (_inputThread == null) return;
8185

82-
var buffer = new char[300];
83-
var current = await _input.ReadBlockAsync(buffer, 0, MinBuffer);
84-
while (current < MinBuffer || buffer[current - 4] != CR || buffer[current - 3] != LF ||
86+
var buffer = new byte[300];
87+
var current = await _input.ReadAsync(buffer, 0, MinBuffer);
88+
while (current < MinBuffer ||
89+
buffer[current - 4] != CR || buffer[current - 3] != LF ||
8590
buffer[current - 2] != CR || buffer[current - 1] != LF)
8691
{
87-
current += await _input.ReadBlockAsync(buffer, current, 1);
92+
current += await _input.ReadAsync(buffer, current, 1);
8893
}
8994

90-
var headersContent = new string(buffer, 0, current);
95+
var headersContent = System.Text.Encoding.ASCII.GetString(buffer, 0, current);
9196
var headers = headersContent.Split(HeaderKeys, StringSplitOptions.RemoveEmptyEntries);
9297
long length = 0;
9398
for (var i = 0; i < headers.Length; i += 2)
@@ -100,11 +105,11 @@ private async void ProcessInputStream()
100105
}
101106
}
102107

103-
var requestBuffer = new char[length];
108+
var requestBuffer = new byte[length];
104109

105-
await _input.ReadBlockAsync(requestBuffer, 0, requestBuffer.Length);
110+
await _input.ReadAsync(requestBuffer, 0, requestBuffer.Length);
106111

107-
var payload = new string(requestBuffer);
112+
var payload = System.Text.Encoding.UTF8.GetString(requestBuffer);
108113

109114
HandleRequest(payload);
110115
}

src/JsonRpc/OutputHandler.cs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@ namespace JsonRpc
99
{
1010
public class OutputHandler : IOutputHandler
1111
{
12-
private readonly TextWriter _output;
12+
private readonly Stream _output;
1313
private Thread _thread;
1414
private readonly BlockingCollection<object> _queue;
1515
private readonly CancellationTokenSource _cancel;
1616

17-
public OutputHandler(TextWriter output)
17+
public OutputHandler(Stream output)
1818
{
19+
if (!output.CanWrite) throw new ArgumentException($"must provide a writable stream for {nameof(output)}", nameof(output));
1920
_output = output;
2021
_queue = new BlockingCollection<object>();
2122
_cancel = new CancellationTokenSource();
@@ -45,14 +46,21 @@ private void ProcessOutputQueue()
4546
if (_queue.TryTake(out var value, Timeout.Infinite, token))
4647
{
4748
var content = JsonConvert.SerializeObject(value);
49+
var contentBytes = System.Text.Encoding.UTF8.GetBytes(content);
4850

4951
// TODO: Is this lsp specific??
5052
var sb = new StringBuilder();
51-
sb.Append($"Content-Length: {content.Length}\r\n");
53+
sb.Append($"Content-Length: {contentBytes.Length}\r\n");
5254
sb.Append($"\r\n");
53-
sb.Append(content);
55+
var headerBytes = System.Text.Encoding.UTF8.GetBytes(sb.ToString());
5456

55-
_output.Write(sb.ToString());
57+
// only one write to _output
58+
using (var ms = new MemoryStream(headerBytes.Length + contentBytes.Length))
59+
{
60+
ms.Write(headerBytes, 0, headerBytes.Length);
61+
ms.Write(contentBytes, 0, contentBytes.Length);
62+
_output.Write(ms.ToArray(), 0, (int)ms.Position);
63+
}
5664
}
5765
}
5866
catch (OperationCanceledException) { }

src/Lsp/LanguageServer.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,13 @@ public class LanguageServer : ILanguageServer, IInitializeHandler, IInitializedH
2929
private readonly TaskCompletionSource<InitializeResult> _initializeComplete = new TaskCompletionSource<InitializeResult>();
3030
private readonly CompositeDisposable _disposable = new CompositeDisposable();
3131

32-
public LanguageServer(TextReader input, TextWriter output)
32+
public LanguageServer(Stream input, Stream output)
3333
: this(input, new OutputHandler(output), new LspReciever(), new RequestProcessIdentifier())
3434
{
3535
}
3636

3737
internal LanguageServer(
38-
TextReader input,
38+
Stream input,
3939
IOutputHandler output,
4040
LspReciever reciever,
4141
IRequestProcessIdentifier requestProcessIdentifier

test/JsonRpc.Tests/InputHandlerTests.cs

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ namespace JsonRpc.Tests
1919
public class InputHandlerTests
2020
{
2121
private static InputHandler NewHandler(
22-
TextReader inputStream,
22+
Stream inputStream,
2323
IOutputHandler outputHandler,
2424
IReciever reciever,
2525
IRequestProcessIdentifier requestProcessIdentifier,
@@ -54,7 +54,7 @@ public void ShouldPassInRequests()
5454
var reciever = Substitute.For<IReciever>();
5555

5656
using (NewHandler(
57-
new StreamReader(inputStream),
57+
inputStream,
5858
outputHandler,
5959
reciever,
6060
Substitute.For<IRequestProcessIdentifier>(),
@@ -71,6 +71,32 @@ public void ShouldPassInRequests()
7171
}
7272
}
7373

74+
[Fact]
75+
public void ShouldPassInUtf8EncodedRequests()
76+
{
77+
// Note: an ä (&auml;) is encoded by two bytes, so string-length is 13 and byte-length is 14
78+
var inputStream = new MemoryStream(Encoding.UTF8.GetBytes("Content-Length: 14\r\n\r\n{\"utf8\": \"ä\"}"));
79+
var outputHandler = Substitute.For<IOutputHandler>();
80+
var reciever = Substitute.For<IReciever>();
81+
82+
using (NewHandler(
83+
inputStream,
84+
outputHandler,
85+
reciever,
86+
Substitute.For<IRequestProcessIdentifier>(),
87+
Substitute.For<IRequestRouter>(),
88+
Substitute.For<IResponseRouter>(),
89+
cts => {
90+
reciever.When(x => x.IsValid(Arg.Any<JToken>()))
91+
.Do(x => {
92+
cts.Cancel();
93+
});
94+
}))
95+
{
96+
reciever.Received().IsValid(Arg.Is<JToken>(x => x["utf8"].ToString() == "ä"));
97+
}
98+
}
99+
74100
[Fact]
75101
public void ShouldHandleRequest()
76102
{
@@ -90,7 +116,7 @@ public void ShouldHandleRequest()
90116
.Returns(response);
91117

92118
using (NewHandler(
93-
new StreamReader(inputStream),
119+
inputStream,
94120
outputHandler,
95121
reciever,
96122
Substitute.For<IRequestProcessIdentifier>(),
@@ -122,7 +148,7 @@ public void ShouldHandleError()
122148

123149

124150
using (NewHandler(
125-
new StreamReader(inputStream),
151+
inputStream,
126152
outputHandler,
127153
reciever,
128154
Substitute.For<IRequestProcessIdentifier>(),
@@ -153,7 +179,7 @@ public void ShouldHandleNotification()
153179
.Returns(c => (new Renor[] { notification }, false));
154180

155181
using (NewHandler(
156-
new StreamReader(inputStream),
182+
inputStream,
157183
outputHandler,
158184
reciever,
159185
Substitute.For<IRequestProcessIdentifier>(),
@@ -187,7 +213,7 @@ public void ShouldHandleResponse()
187213
responseRouter.GetRequest(1L).Returns(tcs);
188214

189215
using (NewHandler(
190-
new StreamReader(inputStream),
216+
inputStream,
191217
outputHandler,
192218
reciever,
193219
Substitute.For<IRequestProcessIdentifier>(),

test/JsonRpc.Tests/OutputHandlerTests.cs

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,21 @@
33
using System.Threading;
44
using System.Threading.Tasks;
55
using NSubstitute;
6+
using FluentAssertions;
67
using Xunit;
78

89
namespace JsonRpc.Tests
910
{
1011
public class OutputHandlerTests
1112
{
12-
private static (OutputHandler handler, Func<Task> wait) NewHandler(TextWriter textWriter,Action<CancellationTokenSource> action)
13+
private static (OutputHandler handler, Func<Task> wait) NewHandler(Stream Writer,Action<CancellationTokenSource> action)
1314
{
1415
var cts = new CancellationTokenSource();
1516
if (!System.Diagnostics.Debugger.IsAttached)
1617
cts.CancelAfter(TimeSpan.FromSeconds(120));
1718
action(cts);
1819

19-
var handler = new OutputHandler(textWriter);
20+
var handler = new OutputHandler(Writer);
2021
handler.Start();
2122
return (handler, () => {
2223
cts.Wait();
@@ -27,11 +28,16 @@ private static (OutputHandler handler, Func<Task> wait) NewHandler(TextWriter te
2728
[Fact]
2829
public async Task ShouldSerializeValues()
2930
{
30-
var tw = Substitute.For<TextWriter>();
31+
var w = Substitute.For<Stream>();
32+
var received = "";
33+
w.CanWrite.Returns(true);
3134

32-
var (handler, wait) = NewHandler(tw, cts => {
33-
tw.When(x => x.Write(Arg.Any<string>()))
34-
.Do(c => cts.Cancel());
35+
var (handler, wait) = NewHandler(w, cts => {
36+
w.When(x => x.Write(Arg.Any<byte[]>(), Arg.Any<int>(), Arg.Any<int>()))
37+
.Do(c => {
38+
received = System.Text.Encoding.UTF8.GetString(c.ArgAt<byte[]>(0), 0, c.ArgAt<int>(2));
39+
cts.Cancel();
40+
});
3541
});
3642
var value = new JsonRpc.Client.Response(1);
3743

@@ -40,8 +46,10 @@ public async Task ShouldSerializeValues()
4046

4147
handler.Send(value);
4248
await wait();
43-
44-
tw.Received().Write("Content-Length: 46\r\n\r\n{\"protocolVersion\":\"2.0\",\"id\":1,\"result\":null}");
49+
const string send = "Content-Length: 46\r\n\r\n{\"protocolVersion\":\"2.0\",\"id\":1,\"result\":null}";
50+
received.Should().Be(send);
51+
var b = System.Text.Encoding.UTF8.GetBytes(send);
52+
w.Received().Write(Arg.Any<byte[]>(), 0, b.Length); // can't compare b here, because it is only value-equal and this test tests reference equality
4553
}
4654
}
4755
}

0 commit comments

Comments
 (0)