Always allow full filesystem access to LSP.

This commit is contained in:
Christian Parpart 2022-04-25 15:35:41 +02:00
parent 49d27eaa5d
commit f308f1a1f8
12 changed files with 350 additions and 136 deletions

View File

@ -8,6 +8,7 @@ Compiler Features:
* Assembly-Json: Export: Include source list in `sourceList` field.
* Commandline Interface: option ``--pretty-json`` works also with the following options: ``--abi``, ``--asm-json``, ``--ast-compact-json``, ``--devdoc``, ``--storage-layout``, ``--userdoc``.
* SMTChecker: Support ``abi.encodeCall`` taking into account the called selector.
* Language Server: Allow full filesystem access to language server.
Bugfixes:

View File

@ -176,3 +176,4 @@ set(sources
add_library(solidity ${sources})
target_link_libraries(solidity PUBLIC yul evmasm langutil smtutil solutil Boost::boost fmt::fmt-header-only)

View File

@ -17,48 +17,130 @@
// SPDX-License-Identifier: GPL-3.0
#include <libsolidity/lsp/FileRepository.h>
#include <libsolidity/lsp/Utils.h>
#include <libsolutil/StringUtils.h>
#include <libsolutil/CommonIO.h>
#include <range/v3/range/conversion.hpp>
#include <range/v3/view/transform.hpp>
#include <regex>
using namespace std;
using namespace solidity;
using namespace solidity::lsp;
using namespace solidity::frontend;
namespace
{
using solidity::util::readFileAsString;
using solidity::util::joinHumanReadable;
string stripFilePrefix(string const& _path)
FileRepository::FileRepository(boost::filesystem::path _basePath): m_basePath(std::move(_basePath))
{
if (_path.find("file://") == 0)
return _path.substr(7);
else
return _path;
}
}
string FileRepository::sourceUnitNameToClientPath(string const& _sourceUnitName) const
string FileRepository::sourceUnitNameToUri(string const& _sourceUnitName) const
{
if (m_sourceUnitNamesToClientPaths.count(_sourceUnitName))
return m_sourceUnitNamesToClientPaths.at(_sourceUnitName);
regex const windowsDriveLetterPath("^[a-zA-Z]:/");
if (m_sourceUnitNamesToUri.count(_sourceUnitName))
return m_sourceUnitNamesToUri.at(_sourceUnitName);
else if (_sourceUnitName.find("file://") == 0)
return _sourceUnitName;
else if (regex_search(_sourceUnitName, windowsDriveLetterPath))
return "file:///" + _sourceUnitName;
else if (_sourceUnitName.find("/") == 0)
return "file://" + _sourceUnitName;
else
return "file://" + (m_fileReader.basePath() / _sourceUnitName).generic_string();
return "file://" + m_basePath.generic_string() + "/" + _sourceUnitName;
}
string FileRepository::clientPathToSourceUnitName(string const& _path) const
string FileRepository::uriToSourceUnitName(string const& _path) const
{
return m_fileReader.cliPathToSourceUnitName(stripFilePrefix(_path));
return stripFileUriSchemePrefix(_path);
}
map<string, string> const& FileRepository::sourceUnits() const
{
return m_fileReader.sourceUnits();
}
void FileRepository::setSourceByClientPath(string const& _uri, string _text)
void FileRepository::setSourceByUri(string const& _uri, string _source)
{
// This is needed for uris outside the base path. It can lead to collisions,
// but we need to mostly rewrite this in a future version anyway.
m_sourceUnitNamesToClientPaths.emplace(clientPathToSourceUnitName(_uri), _uri);
m_fileReader.addOrUpdateFile(stripFilePrefix(_uri), move(_text));
auto sourceUnitName = uriToSourceUnitName(_uri);
m_sourceUnitNamesToUri.emplace(sourceUnitName, _uri);
m_sourceCodes[sourceUnitName] = std::move(_source);
}
frontend::ReadCallback::Result FileRepository::readFile(string const& _kind, string const& _sourceUnitName)
{
solAssert(
_kind == ReadCallback::kindString(ReadCallback::Kind::ReadFile),
"ReadFile callback used as callback kind " + _kind
);
try
{
// File was read already. Use local store.
if (m_sourceCodes.count(_sourceUnitName))
return ReadCallback::Result{true, m_sourceCodes.at(_sourceUnitName)};
string const strippedSourceUnitName = stripFileUriSchemePrefix(_sourceUnitName);
if (
boost::filesystem::path(strippedSourceUnitName).has_root_path() &&
boost::filesystem::exists(strippedSourceUnitName)
)
{
auto contents = readFileAsString(strippedSourceUnitName);
solAssert(m_sourceCodes.count(_sourceUnitName) == 0, "");
m_sourceCodes[_sourceUnitName] = contents;
return ReadCallback::Result{true, move(contents)};
}
vector<boost::filesystem::path> candidates;
vector<reference_wrapper<boost::filesystem::path>> prefixes = {m_basePath};
prefixes += (m_includePaths | ranges::to<vector<reference_wrapper<boost::filesystem::path>>>);
auto const pathToQuotedString = [](boost::filesystem::path const& _path) { return "\"" + _path.string() + "\""; };
for (auto const& prefix: prefixes)
{
boost::filesystem::path canonicalPath = boost::filesystem::path(prefix) / boost::filesystem::path(strippedSourceUnitName);
if (boost::filesystem::exists(canonicalPath))
candidates.push_back(move(canonicalPath));
}
if (candidates.empty())
return ReadCallback::Result{
false,
"File not found. Searched the following locations: " +
joinHumanReadable(prefixes | ranges::views::transform(pathToQuotedString), ", ") +
"."
};
if (candidates.size() >= 2)
return ReadCallback::Result{
false,
"Ambiguous import. "
"Multiple matching files found inside base path and/or include paths: " +
joinHumanReadable(candidates | ranges::views::transform(pathToQuotedString), ", ") +
"."
};
if (!boost::filesystem::is_regular_file(candidates[0]))
return ReadCallback::Result{false, "Not a valid file."};
auto contents = readFileAsString(candidates[0]);
solAssert(m_sourceCodes.count(_sourceUnitName) == 0, "");
m_sourceCodes[_sourceUnitName] = contents;
return ReadCallback::Result{true, move(contents)};
}
catch (std::exception const& _exception)
{
return ReadCallback::Result{false, "Exception in read callback: " + boost::diagnostic_information(_exception)};
}
catch (...)
{
return ReadCallback::Result{false, "Unknown exception in read callback: " + boost::current_exception_diagnostic_information()};
}
}

View File

@ -28,26 +28,42 @@ namespace solidity::lsp
class FileRepository
{
public:
explicit FileRepository(boost::filesystem::path const& _basePath):
m_fileReader(_basePath) {}
explicit FileRepository(boost::filesystem::path _basePath);
boost::filesystem::path const& basePath() const { return m_fileReader.basePath(); }
boost::filesystem::path const& basePath() const { return m_basePath; }
/// Translates a compiler-internal source unit name to an LSP client path.
std::string sourceUnitNameToClientPath(std::string const& _sourceUnitName) const;
/// Translates an LSP client path into a compiler-internal source unit name.
std::string clientPathToSourceUnitName(std::string const& _uri) const;
std::string sourceUnitNameToUri(std::string const& _sourceUnitName) const;
/// Translates an LSP file URI into a compiler-internal source unit name.
std::string uriToSourceUnitName(std::string const& _uri) const;
/// @returns all sources by their compiler-internal source unit name.
std::map<std::string, std::string> const& sourceUnits() const;
/// Changes the source identified by the LSP client path _uri to _text.
void setSourceByClientPath(std::string const& _uri, std::string _text);
StringMap const& sourceUnits() const noexcept { return m_sourceCodes; }
frontend::ReadCallback::Callback reader() { return m_fileReader.reader(); }
/// Changes the source identified by the LSP client path _uri to _text.
void setSourceByUri(std::string const& _uri, std::string _text);
void addOrUpdateFile(boost::filesystem::path const& _path, frontend::SourceCode _source);
void setSourceUnits(StringMap _sources);
frontend::ReadCallback::Result readFile(std::string const& _kind, std::string const& _sourceUnitName);
frontend::ReadCallback::Callback reader()
{
return [this](std::string const& _kind, std::string const& _path) { return readFile(_kind, _path); };
}
private:
std::map<std::string, std::string> m_sourceUnitNamesToClientPaths;
frontend::FileReader m_fileReader;
/// Base path without URI scheme.
boost::filesystem::path m_basePath;
/// Additional directories used for resolving relative paths in imports.
std::vector<boost::filesystem::path> m_includePaths;
/// Mapping of source unit names to their URIs as understood by the client.
StringMap m_sourceUnitNamesToUri;
/// Mapping of source unit names to their file content.
StringMap m_sourceCodes;
};
}

View File

@ -47,7 +47,7 @@ Json::Value HandlerBase::toJson(SourceLocation const& _location) const
{
solAssert(_location.sourceName);
Json::Value item = Json::objectValue;
item["uri"] = fileRepository().sourceUnitNameToClientPath(*_location.sourceName);
item["uri"] = fileRepository().sourceUnitNameToUri(*_location.sourceName);
item["range"] = toRange(_location);
return item;
}
@ -55,7 +55,7 @@ Json::Value HandlerBase::toJson(SourceLocation const& _location) const
pair<string, LineColumn> HandlerBase::extractSourceUnitNameAndLineColumn(Json::Value const& _args) const
{
string const uri = _args["textDocument"]["uri"].asString();
string const sourceUnitName = fileRepository().clientPathToSourceUnitName(uri);
string const sourceUnitName = fileRepository().uriToSourceUnitName(uri);
if (!fileRepository().sourceUnits().count(sourceUnitName))
BOOST_THROW_EXCEPTION(
RequestError(ErrorCode::RequestFailed) <<

View File

@ -37,8 +37,6 @@
#include <boost/filesystem.hpp>
#include <boost/algorithm/string/predicate.hpp>
#include <fmt/format.h>
#include <ostream>
#include <string>
@ -114,9 +112,9 @@ void LanguageServer::compile()
swap(oldRepository, m_fileRepository);
for (string const& fileName: m_openFiles)
m_fileRepository.setSourceByClientPath(
m_fileRepository.setSourceByUri(
fileName,
oldRepository.sourceUnits().at(oldRepository.clientPathToSourceUnitName(fileName))
oldRepository.sourceUnits().at(oldRepository.uriToSourceUnitName(fileName))
);
// TODO: optimize! do not recompile if nothing has changed (file(s) not flagged dirty).
@ -178,7 +176,7 @@ void LanguageServer::compileAndUpdateDiagnostics()
for (auto&& [sourceUnitName, diagnostics]: diagnosticsBySourceUnit)
{
Json::Value params;
params["uri"] = m_fileRepository.sourceUnitNameToClientPath(sourceUnitName);
params["uri"] = m_fileRepository.sourceUnitNameToUri(sourceUnitName);
if (!diagnostics.empty())
m_nonemptyDiagnostics.insert(sourceUnitName);
params["diagnostics"] = move(diagnostics);
@ -252,13 +250,12 @@ void LanguageServer::handleInitialize(MessageID _id, Json::Value const& _args)
ErrorCode::InvalidParams,
"rootUri only supports file URI scheme."
);
rootPath = rootPath.substr(7);
rootPath = stripFileUriSchemePrefix(rootPath);
}
else if (Json::Value rootPath = _args["rootPath"])
rootPath = rootPath.asString();
m_fileRepository = FileRepository(boost::filesystem::path(rootPath));
m_fileRepository = FileRepository(rootPath);
if (_args["initializationOptions"].isObject())
changeConfiguration(_args["initializationOptions"]);
@ -309,7 +306,7 @@ void LanguageServer::handleTextDocumentDidOpen(Json::Value const& _args)
string text = _args["textDocument"]["text"].asString();
string uri = _args["textDocument"]["uri"].asString();
m_openFiles.insert(uri);
m_fileRepository.setSourceByClientPath(uri, move(text));
m_fileRepository.setSourceByUri(uri, move(text));
compileAndUpdateDiagnostics();
}
@ -327,7 +324,7 @@ void LanguageServer::handleTextDocumentDidChange(Json::Value const& _args)
"Invalid content reference."
);
string const sourceUnitName = m_fileRepository.clientPathToSourceUnitName(uri);
string const sourceUnitName = m_fileRepository.uriToSourceUnitName(uri);
lspAssert(
m_fileRepository.sourceUnits().count(sourceUnitName),
ErrorCode::RequestFailed,
@ -348,7 +345,7 @@ void LanguageServer::handleTextDocumentDidChange(Json::Value const& _args)
buffer.replace(static_cast<size_t>(change->start), static_cast<size_t>(change->end - change->start), move(text));
text = move(buffer);
}
m_fileRepository.setSourceByClientPath(uri, move(text));
m_fileRepository.setSourceByUri(uri, move(text));
}
compileAndUpdateDiagnostics();

View File

@ -92,7 +92,7 @@ private:
Transport& m_client;
std::map<std::string, MessageHandler> m_handlers;
/// Set of files known to be open by the client.
/// Set of files (names in URI form) known to be open by the client.
std::set<std::string> m_openFiles;
/// Set of source unit names for which we sent diagnostics to the client in the last iteration.
std::set<std::string> m_nonemptyDiagnostics;

View File

@ -22,31 +22,25 @@
#include <libsolutil/CommonIO.h>
#include <liblangutil/Exceptions.h>
#include <fmt/format.h>
#include <boost/algorithm/string.hpp>
#include <iostream>
#include <sstream>
#include <string>
#if defined(_WIN32)
#include <io.h>
#include <fcntl.h>
#endif
using namespace std;
using namespace solidity::lsp;
IOStreamTransport::IOStreamTransport(istream& _in, ostream& _out):
m_input{_in},
m_output{_out}
{
}
IOStreamTransport::IOStreamTransport():
IOStreamTransport(cin, cout)
{
}
bool IOStreamTransport::closed() const noexcept
{
return m_input.eof();
}
optional<Json::Value> IOStreamTransport::receive()
// {{{ Transport
optional<Json::Value> Transport::receive()
{
auto const headers = parseHeaders();
if (!headers)
@ -61,7 +55,7 @@ optional<Json::Value> IOStreamTransport::receive()
return nullopt;
}
string const data = util::readBytes(m_input, stoul(headers->at("content-length")));
string const data = readBytes(stoul(headers->at("content-length")));
Json::Value jsonMessage;
string jsonParsingErrors;
@ -75,29 +69,6 @@ optional<Json::Value> IOStreamTransport::receive()
return {move(jsonMessage)};
}
void IOStreamTransport::notify(string _method, Json::Value _message)
{
Json::Value json;
json["method"] = move(_method);
json["params"] = move(_message);
send(move(json));
}
void IOStreamTransport::reply(MessageID _id, Json::Value _message)
{
Json::Value json;
json["result"] = move(_message);
send(move(json), _id);
}
void IOStreamTransport::error(MessageID _id, ErrorCode _code, string _message)
{
Json::Value json;
json["error"]["code"] = static_cast<int>(_code);
json["error"]["message"] = move(_message);
send(move(json), _id);
}
void Transport::trace(std::string _message, Json::Value _extra)
{
if (m_logTrace != TraceValue::Off)
@ -110,30 +81,13 @@ void Transport::trace(std::string _message, Json::Value _extra)
}
}
void IOStreamTransport::send(Json::Value _json, MessageID _id)
{
solAssert(_json.isObject());
_json["jsonrpc"] = "2.0";
if (_id != Json::nullValue)
_json["id"] = _id;
string const jsonString = solidity::util::jsonCompactPrint(_json);
m_output << "Content-Length: " << jsonString.size() << "\r\n";
m_output << "\r\n";
m_output << jsonString;
m_output.flush();
}
optional<map<string, string>> IOStreamTransport::parseHeaders()
optional<map<string, string>> Transport::parseHeaders()
{
map<string, string> headers;
while (true)
{
string line;
getline(m_input, line);
auto line = getline();
if (boost::trim_copy(line).empty())
break;
@ -141,13 +95,127 @@ optional<map<string, string>> IOStreamTransport::parseHeaders()
if (delimiterPos == string::npos)
return nullopt;
string name = boost::to_lower_copy(line.substr(0, delimiterPos));
string value = line.substr(delimiterPos + 1);
if (!headers.emplace(
boost::trim_copy(name),
boost::trim_copy(value)
).second)
auto const name = boost::to_lower_copy(line.substr(0, delimiterPos));
auto const value = line.substr(delimiterPos + 1);
if (!headers.emplace(boost::trim_copy(name), boost::trim_copy(value)).second)
return nullopt;
}
return {move(headers)};
}
void Transport::notify(string _method, Json::Value _message)
{
Json::Value json;
json["method"] = move(_method);
json["params"] = move(_message);
send(move(json));
}
void Transport::reply(MessageID _id, Json::Value _message)
{
Json::Value json;
json["result"] = move(_message);
send(move(json), _id);
}
void Transport::error(MessageID _id, ErrorCode _code, string _message)
{
Json::Value json;
json["error"]["code"] = static_cast<int>(_code);
json["error"]["message"] = move(_message);
send(move(json), _id);
}
void Transport::send(Json::Value _json, MessageID _id)
{
solAssert(_json.isObject());
_json["jsonrpc"] = "2.0";
if (_id != Json::nullValue)
_json["id"] = _id;
// Trailing CRLF only for easier readability.
string const jsonString = solidity::util::jsonCompactPrint(_json);
writeBytes(fmt::format("Content-Length: {}\r\n\r\n", jsonString.size()));
writeBytes(jsonString);
flushOutput();
}
// }}}
// {{{ IOStreamTransport
IOStreamTransport::IOStreamTransport(istream& _in, ostream& _out):
m_input{_in},
m_output{_out}
{
}
bool IOStreamTransport::closed() const noexcept
{
return m_input.eof();
}
std::string IOStreamTransport::readBytes(size_t _length)
{
return util::readBytes(m_input, _length);
}
std::string IOStreamTransport::getline()
{
string line;
std::getline(m_input, line);
return line;
}
void IOStreamTransport::writeBytes(std::string_view _data)
{
m_output.write(_data.data(), static_cast<std::streamsize>(_data.size()));
}
void IOStreamTransport::flushOutput()
{
m_output.flush();
}
// }}}
// {{{ StdioTransport
StdioTransport::StdioTransport()
{
#if defined(_WIN32)
// Attempt to change the modes of stdout from text to binary.
setmode(fileno(stdout), O_BINARY);
#endif
}
bool StdioTransport::closed() const noexcept
{
return feof(stdin);
}
std::string StdioTransport::readBytes(size_t _byteCount)
{
std::string buffer;
buffer.resize(_byteCount);
auto const n = fread(buffer.data(), 1, _byteCount, stdin);
if (n < _byteCount)
buffer.resize(n);
return buffer;
}
std::string StdioTransport::getline()
{
std::string line;
std::getline(std::cin, line);
return line;
}
void StdioTransport::writeBytes(std::string_view _data)
{
auto const bytesWritten = fwrite(_data.data(), 1, _data.size(), stdout);
solAssert(bytesWritten == _data.size());
}
void StdioTransport::flushOutput()
{
fflush(stdout);
}
// }}}

View File

@ -91,20 +91,44 @@ class Transport
public:
virtual ~Transport() = default;
std::optional<Json::Value> receive();
void notify(std::string _method, Json::Value _params);
void reply(MessageID _id, Json::Value _result);
void error(MessageID _id, ErrorCode _code, std::string _message);
virtual bool closed() const noexcept = 0;
virtual std::optional<Json::Value> receive() = 0;
virtual void notify(std::string _method, Json::Value _params) = 0;
virtual void reply(MessageID _id, Json::Value _result) = 0;
virtual void error(MessageID _id, ErrorCode _code, std::string _message) = 0;
void trace(std::string _message, Json::Value _extra = Json::nullValue);
TraceValue traceValue() const noexcept { return m_logTrace; }
void setTrace(TraceValue _value) noexcept { m_logTrace = _value; }
private:
TraceValue m_logTrace = TraceValue::Off;
protected:
/// Reads from the transport and parses the headers until the beginning
/// of the contents.
std::optional<std::map<std::string, std::string>> parseHeaders();
/// Consumes exactly @p _byteCount bytes, as needed for consuming
/// the message body from the transport line.
virtual std::string readBytes(size_t _byteCount) = 0;
// Mimmicks std::getline() on this Transport API.
virtual std::string getline() = 0;
/// Writes the given payload @p _data to transport.
/// This call may or may not buffer.
virtual void writeBytes(std::string_view _data) = 0;
/// Ensures transport output is flushed.
virtual void flushOutput() = 0;
/// Sends an arbitrary raw message to the client.
///
/// Used by the notify/reply/error function family.
virtual void send(Json::Value _message, MessageID _id = Json::nullValue);
};
/**
@ -119,27 +143,34 @@ public:
/// @param _out for example std::cout (stdout)
IOStreamTransport(std::istream& _in, std::ostream& _out);
// Constructs a JSON transport using standard I/O streams.
IOStreamTransport();
bool closed() const noexcept override;
std::optional<Json::Value> receive() override;
void notify(std::string _method, Json::Value _params) override;
void reply(MessageID _id, Json::Value _result) override;
void error(MessageID _id, ErrorCode _code, std::string _message) override;
protected:
/// Sends an arbitrary raw message to the client.
///
/// Used by the notify/reply/error function family.
virtual void send(Json::Value _message, MessageID _id = Json::nullValue);
/// Parses header section from the client including message-delimiting empty line.
std::optional<std::map<std::string, std::string>> parseHeaders();
std::string readBytes(size_t _byteCount) override;
std::string getline() override;
void writeBytes(std::string_view _data) override;
void flushOutput() override;
private:
std::istream& m_input;
std::ostream& m_output;
};
/**
* Standard I/O transport Layer utilizing stdin/stdout for communication.
*/
class StdioTransport: public Transport
{
public:
StdioTransport();
bool closed() const noexcept override;
protected:
std::string readBytes(size_t _byteCount) override;
std::string getline() override;
void writeBytes(std::string_view _data) override;
void flushOutput() override;
};
}

View File

@ -22,7 +22,7 @@
#include <libsolidity/lsp/FileRepository.h>
#include <libsolidity/lsp/Utils.h>
#include <fmt/format.h>
#include <regex>
#include <fstream>
namespace solidity::lsp
@ -115,4 +115,15 @@ optional<SourceLocation> parseRange(FileRepository const& _fileRepository, strin
return start;
}
string stripFileUriSchemePrefix(string const& _path)
{
regex const windowsDriveLetterPath("^file:///[a-zA-Z]:/");
if (regex_search(_path, windowsDriveLetterPath))
return _path.substr(8);
if (_path.find("file://") == 0)
return _path.substr(7);
else
return _path;
}
}

View File

@ -64,6 +64,13 @@ std::optional<langutil::SourceLocation> parseRange(
Json::Value const& _range
);
/// Strips the file:// URI prefix off the given path, if present,
/// also taking special care of Windows-drive-letter paths.
///
/// So file:///path/to/some/file.txt returns /path/to/some/file.txt, as well as,
/// file:///C:/file.txt will return C:/file.txt (forward-slash is okay on Windows).
std::string stripFileUriSchemePrefix(std::string const& _path);
/// Extracts the resolved declaration of the given expression AST node.
///
/// This may for example be the type declaration of an identifier,

View File

@ -912,7 +912,7 @@ void CommandLineInterface::handleAst()
void CommandLineInterface::serveLSP()
{
lsp::IOStreamTransport transport;
lsp::StdioTransport transport;
if (!lsp::LanguageServer{transport}.run())
solThrow(CommandLineExecutionError, "LSP terminated abnormally.");
}