Index: clangd/JSONRPCDispatcher.cpp =================================================================== --- clangd/JSONRPCDispatcher.cpp +++ clangd/JSONRPCDispatcher.cpp @@ -135,6 +135,7 @@ void clangd::runLanguageServerLoop(std::istream &In, JSONOutput &Out, JSONRPCDispatcher &Dispatcher, bool &IsDone) { + unsigned long long ContentLength = 0; while (In.good()) { // A Language Server Protocol message starts with a HTTP header, delimited // by \r\n. @@ -145,37 +146,48 @@ continue; } - // Skip empty lines. llvm::StringRef LineRef(Line); - if (LineRef.trim().empty()) - continue; // We allow YAML-style comments. Technically this isn't part of the // LSP specification, but makes writing tests easier. if (LineRef.startswith("#")) continue; - unsigned long long Len = 0; - // FIXME: Content-Type is a specified header, but does nothing. + // Content-Type is a specified header, but does nothing. // Content-Length is a mandatory header. It specifies the length of the // following JSON. - if (LineRef.consume_front("Content-Length: ")) - llvm::getAsUnsignedInteger(LineRef.trim(), 0, Len); - - // Check if the next line only contains \r\n. If not this is another header, - // which we ignore. - char NewlineBuf[2]; - In.read(NewlineBuf, 2); - if (std::memcmp(NewlineBuf, "\r\n", 2) != 0) + // It is unspecified what sequence headers must be supplied in, so we allow + // any sequence. + // The end of headers is signified by an empty line. + if (LineRef.consume_front("Content-Length: ")) { + llvm::getAsUnsignedInteger(LineRef.trim(), 0, ContentLength); + continue; + } else if (!LineRef.trim().empty()) { + // It's another header, ignore it. continue; + } else { + // An empty line indicates the end of headers. + // Go ahead and read the JSON. + } - // Now read the JSON. Insert a trailing null byte as required by the YAML - // parser. - std::vector JSON(Len + 1, '\0'); - In.read(JSON.data(), Len); + if (ContentLength > 0) { + // Now read the JSON. Insert a trailing null byte as required by the YAML + // parser. + std::vector JSON(ContentLength + 1, '\0'); + In.read(JSON.data(), ContentLength); + + // If the stream is aborted before we read ContentLength bytes, In + // will have eofbit and failbit set. + if (!In) { + Out.log("Input was aborted. Read only " + + std::to_string(In.gcount()) + + " bytes of expected " + + std::to_string(ContentLength) + + ".\n"); + break; + } - if (Len > 0) { - llvm::StringRef JSONRef(JSON.data(), Len); + llvm::StringRef JSONRef(JSON.data(), ContentLength); // Log the message. Out.log("<-- " + JSONRef + "\n"); @@ -187,5 +199,10 @@ if (IsDone) break; } + // Else we've read a malformed message of some kind. Ignore it and try to + // reset. + + // We've consume the message. Reset. + ContentLength = 0; } } Index: test/clangd/protocol.test =================================================================== --- /dev/null +++ test/clangd/protocol.test @@ -0,0 +1,81 @@ +# RUN: clangd -run-synchronously < %s | FileCheck %s +# vim: fileformat=dos +# It is absolutely vital that this file has CRLF line endings. +# +# Test protocol parsing +Content-Length: 125 +Content-Type: application/vscode-jsonrpc; charset-utf-8 + +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} +# Test message with Content-Type after Content-Length +# +# CHECK: "jsonrpc":"2.0","id":0,"result":{"capabilities":{ +# CHECK-DAG: "textDocumentSync": 1, +# CHECK-DAG: "documentFormattingProvider": true, +# CHECK-DAG: "documentRangeFormattingProvider": true, +# CHECK-DAG: "documentOnTypeFormattingProvider": {"firstTriggerCharacter":"}","moreTriggerCharacter":[]}, +# CHECK-DAG: "codeActionProvider": true, +# CHECK-DAG: "completionProvider": {"resolveProvider": false, "triggerCharacters": [".",">",":"]}, +# CHECK-DAG: "definitionProvider": true +# CHECK: }} + +Content-Length: 246 + +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"file:///main.cpp","languageId":"cpp","version":1,"text":"struct fake { int a, bb, ccc; int f(int i, const float f) const; };\nint main() {\n fake f;\n f.\n}\n"}}} + +Content-Type: application/vscode-jsonrpc; charset-utf-8 +Content-Length: 146 + +{"jsonrpc":"2.0","id":1,"method":"textDocument/completion","params":{"textDocument":{"uri":"file:/main.cpp"},"position":{"line":3,"character":5}}} +# Test message with Content-Type before Content-Length +# +# CHECK: {"jsonrpc":"2.0","id":1,"result":[ +# CHECK-DAG: {"label":"a","kind":5,"detail":"int","sortText":"00035a","filterText":"a","insertText":"a"} +# CHECK: ]} + +X-Test: Testing +Content-Type: application/vscode-jsonrpc; charset-utf-8 +Content-Length: 146 +Content-Type: application/vscode-jsonrpc; charset-utf-8 +X-Testing: Test + +{"jsonrpc":"2.0","id":2,"method":"textDocument/completion","params":{"textDocument":{"uri":"file:/main.cpp"},"position":{"line":3,"character":5}}} + +Content-Type: application/vscode-jsonrpc; charset-utf-8 +Content-Length: 10 +Content-Length: 146 + +{"jsonrpc":"2.0","id":3,"method":"textDocument/completion","params":{"textDocument":{"uri":"file:/main.cpp"},"position":{"line":3,"character":5}}} +# Test message with duplicate Content-Length headers +# +# CHECK: {"jsonrpc":"2.0","id":3,"result":[ +# CHECK-DAG: {"label":"a","kind":5,"detail":"int","sortText":"00035a","filterText":"a","insertText":"a"} +# CHECK: ]} + +Content-Type: application/vscode-jsonrpc; charset-utf-8 +Content-Length: 10 + +{"jsonrpc":"2.0","id":4,"method":"textDocument/completion","params":{"textDocument":{"uri":"file:/main.cpp"},"position":{"line":3,"character":5}}} +# Test message with malformed Content-Length +# +# stderr will have: JSON dispatch failed! +# Ensure we recover by sending another (valid) message + +Content-Length: 146 + +{"jsonrpc":"2.0","id":5,"method":"textDocument/completion","params":{"textDocument":{"uri":"file:/main.cpp"},"position":{"line":3,"character":5}}} +# Test message with Content-Type before Content-Length +# +# CHECK: {"jsonrpc":"2.0","id":5,"result":[ +# CHECK-DAG: {"label":"a","kind":5,"detail":"int","sortText":"00035a","filterText":"a","insertText":"a"} +# CHECK: ]} + +Content-Length: 1024 + +{"jsonrpc":"2.0","id":5,"method":"textDocument/completion","params":{"textDocument":{"uri":"file:/main.cpp"},"position":{"line":3,"character":5}}} +# Test message which reads beyond the end of the stream. +# +# Ensure this is the last test in the file! +# stderr will have: Input was aborted. Read only {{[0-9]+}} bytes of expected {{[0-9]+}}. +# But we can't check that here. We can exercise the code however. +