From 712e1a2de04a92ab19073cbda3f0a3306a79fbc3 Mon Sep 17 00:00:00 2001 From: Rodrigo Fernandes Date: Wed, 11 Sep 2019 18:09:48 +0100 Subject: [PATCH] Implement DNS query support Implement support for dns request / response with the goal of allowing a asyncronous host -> ip resolution mechanism. * Created a example tool for dns query: ``` $ bin/toolbox-dns -s 8.8.8.8 www.reactivemarkets.com ``` * Added an additional hex_dump debug facility Close #57 --- CMakeLists.txt | 6 +- example/CMakeLists.txt | 3 + example/Dns.cpp | 61 ++++++++ toolbox/CMakeLists.txt | 2 + toolbox/net.hpp | 1 + toolbox/net/Resolver2.cpp | 281 +++++++++++++++++++++++++++++++++++ toolbox/net/Resolver2.hpp | 60 ++++++++ toolbox/net/Resolver2.ut.cpp | 54 +++++++ toolbox/util/Debug.hpp | 68 +++++++++ toolbox/util/Debug.ut.cpp | 43 ++++++ toolbox/util/Stream.cpp | 9 ++ toolbox/util/Stream.hpp | 4 + toolbox/util/Stream.ut.cpp | 10 ++ toolbox/util/Traits.ut.cpp | 2 +- 14 files changed, 600 insertions(+), 4 deletions(-) create mode 100644 example/Dns.cpp create mode 100644 toolbox/net/Resolver2.cpp create mode 100644 toolbox/net/Resolver2.hpp create mode 100644 toolbox/net/Resolver2.ut.cpp create mode 100644 toolbox/util/Debug.hpp create mode 100644 toolbox/util/Debug.ut.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 0a9001b19..e029e2166 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -134,9 +134,9 @@ install_libraries("${Boost_DATE_TIME_LIBRARY}") add_definitions(-DBOOST_NO_AUTO_PTR=1 -DBOOST_NO_RTTI=1 -DBOOST_NO_TYPEID=1) add_definitions(-DBOOST_ASIO_DISABLE_THREADS=1 -DBOOST_ASIO_HEADER_ONLY=1) -if(NOT "${Boost_UNIT_TEST_FRAMEWORK_LIBRARY}" MATCHES "\\.a$") - add_definitions(-DBOOST_TEST_DYN_LINK) -endif() +# if(NOT "${Boost_UNIT_TEST_FRAMEWORK_LIBRARY}" MATCHES "\\.a$") +# add_definitions(-DBOOST_TEST_DYN_LINK) +# endif() find_package(Doxygen) # Optional. diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index 1e6349739..ff1c4d0cc 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -29,3 +29,6 @@ target_link_libraries(toolbox-echo-serv ${toolbox_LIBRARY}) add_executable(toolbox-http-serv HttpServ.cpp) target_link_libraries(toolbox-http-serv ${toolbox_LIBRARY}) + +add_executable(toolbox-dns Dns.cpp) +target_link_libraries(toolbox-dns ${toolbox_LIBRARY}) diff --git a/example/Dns.cpp b/example/Dns.cpp new file mode 100644 index 000000000..5c0257eb1 --- /dev/null +++ b/example/Dns.cpp @@ -0,0 +1,61 @@ +// The Reactive C++ Toolbox. +// Copyright (C) 2013-2019 Swirly Cloud Limited +// Copyright (C) 2019 Reactive Markets Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include + +#include +#include + +using namespace std; +using namespace toolbox; +using namespace toolbox::net; + +int main(int argc, char* argv[]) +{ + std::ios::sync_with_stdio(false); + try { + + std::string nameserver; + std::string hostname; + + Options opts{"Unit Test options [OPTIONS] [COMMAND]"}; + // clang-format off + opts('s', Value{nameserver}, "ShortOption Description") + ('h', "help", Help{}, "Help") + (Value{hostname}.required(), "Hostname") + ; + // clang-format on + + opts.parse(argc, argv); + + if (nameserver.empty()) { + std::ifstream resolv_conf{"/etc/resolv.conf"}; + DnsServers dns_servers{resolv_conf}; + nameserver = dns_servers.server(); + } + + const auto ip = toolbox::net::get_host_by_name(nameserver, hostname, DnsRequest::A); + cout << ip << endl; + + } catch (const std::exception& e) { + TOOLBOX_ERROR << "exception: " << e.what(); + return 1; + } + return 0; +} diff --git a/toolbox/CMakeLists.txt b/toolbox/CMakeLists.txt index 9c9bba390..eba2a83af 100644 --- a/toolbox/CMakeLists.txt +++ b/toolbox/CMakeLists.txt @@ -77,6 +77,7 @@ set(lib_SOURCES net/McastSock.cpp net/Protocol.cpp net/Resolver.cpp + net/Resolver2.cpp net/Runner.cpp net/Socket.cpp net/StreamAcceptor.cpp @@ -218,6 +219,7 @@ set(test_SOURCES net/Endpoint.ut.cpp net/IoSock.ut.cpp net/Resolver.ut.cpp + net/Resolver2.ut.cpp net/Runner.ut.cpp net/Socket.ut.cpp sys/Date.ut.cpp diff --git a/toolbox/net.hpp b/toolbox/net.hpp index 072907a5f..e1f08bc95 100644 --- a/toolbox/net.hpp +++ b/toolbox/net.hpp @@ -25,6 +25,7 @@ #include "net/McastSock.hpp" #include "net/Protocol.hpp" #include "net/Resolver.hpp" +#include "net/Resolver2.hpp" #include "net/Runner.hpp" #include "net/Socket.hpp" #include "net/StreamAcceptor.hpp" diff --git a/toolbox/net/Resolver2.cpp b/toolbox/net/Resolver2.cpp new file mode 100644 index 000000000..63fe708d3 --- /dev/null +++ b/toolbox/net/Resolver2.cpp @@ -0,0 +1,281 @@ +// The Reactive C++ Toolbox. +// Copyright (C) 2013-2019 Swirly Cloud Limited +// Copyright (C) 2019 Reactive Markets Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "Resolver2.hpp" + +#include "Endpoint.hpp" +#include "Socket.hpp" + +#include + +#include + +#include +#include +#include + +namespace toolbox { +inline namespace net { + +char* to_dns(const std::string& host, char* buff) +{ + std::size_t pos{0}; + std::string_view view{host}; + while ((pos = view.find_first_of('.'), pos) != std::string_view::npos) { + *buff = static_cast(pos); + std::memcpy(++buff, view.data(), pos); + view.remove_prefix(pos + 1); // Skip the dot + buff += pos; + } + *buff = static_cast(view.size()); + std::memcpy(++buff, view.data(), view.size() + 1); // Include null terminator + return buff += view.size() + 1; +} + +DnsServers::DnsServers(std::istream& resolv_conf, std::vector additional_nameservers) +: servers_{std::move(additional_nameservers)} +{ + std::string line; + const std::string nameserver{"nameserver"}; + while (std::getline(resolv_conf, line)) { + // C++20 Replace with starts_with + if (line.compare(0, nameserver.size(), nameserver) == 0) { + auto pos = line.find_first_not_of(' ', nameserver.size() + 1); + if (pos != std::string::npos) { + servers_.emplace_back(line.substr(pos)); + } + } + } + + if (servers_.empty()) { + throw std::runtime_error("No servers available"); + } +} + +const DnsServers::Servers& DnsServers::servers() const noexcept +{ + return servers_; +} + +const std::string& DnsServers::server() const +{ + return servers_[0]; +} + +#pragma pack(push, 1) + +class Dns { + public: + Dns(const std::string& hostname, DnsRequest::Type query_type) + { + auto res = to_dns(hostname, query_name()); + hostname_size_ = res - query_name(); + question().qtype = htons(query_type); + question().class_ = htons(1); // Internet + } + + class Header { + + public: + Header() + : id_{(unsigned short)htons(getpid())} + , recursion_desired_{1} + , truncated_{0} + , authorative_answer_{0} + , opcode_{0} // Standard query + , qr_{0} // Query + , rcode_{0} + , cd_{0} + , ad_{0} + , z_{0} + , recursion_available_{0} + , questions_{htons(1)} // Single question + , ans_count_{0} + , auth_count_{0} + , add_count_{0} + { + } + + bool recursion_desired() const noexcept { return recursion_desired_; } + bool truncated() const noexcept { return truncated_; } + bool authorative_answer() const noexcept { return authorative_answer_; } + auto opcode() const noexcept { return opcode_; } + bool qr() const noexcept { return qr_; } + auto rcode() const noexcept { return rcode_; } + bool cd() const noexcept { return cd_; } + bool ad() const noexcept { return ad_; } + bool z() const noexcept { return z_; } + bool recursion_available() const noexcept { return recursion_available_; } + + auto questions() const noexcept { return ntohs(questions_); } + auto ans_count() const noexcept { return ntohs(ans_count_); } + auto auth_count() const noexcept { return ntohs(auth_count_); } + auto add_count() const noexcept { return ntohs(add_count_); } + + private: + // C++20 will allow bitfield initialization on declaration + uint16_t id_; // message id + uint8_t recursion_desired_ : 1; // recursion desired + uint8_t truncated_ : 1; // truncated message + uint8_t authorative_answer_ : 1; // authoritive answer + uint8_t opcode_ : 4; // purpose of message + uint8_t qr_ : 1; // query/response flag + + uint8_t rcode_ : 4; // response code + uint8_t cd_ : 1; // disable checking + uint8_t ad_ : 1; // authenticated data + uint8_t z_ : 1; // its z! reserveddns_servers + uint8_t recursion_available_ : 1; // recursion available + + uint16_t questions_; // number of question entries + uint16_t ans_count_; // number of answer entries + uint16_t auth_count_; // number of authority entries + uint16_t add_count_; // number of resource entries + }; + + struct Question { + uint16_t qtype; + uint16_t class_; + }; + + class RData { + public: + enum class Type : uint16_t { + A_RECORD = 0x0001, + NAME_SERVER = 0x0002, + CNAME = 0x0005, + MAIL_SERVER = 0x000f + }; + + Type type() const noexcept { return static_cast(ntohs(type_)); } + auto class_() const noexcept { return ntohs(class__); } + auto ttl() const noexcept { return ntohs(ttl_); } + auto length() const noexcept { return ntohs(length_); } + + private: + uint16_t type_; + uint16_t class__; + uint32_t ttl_; + uint16_t length_; + }; + + const auto& header() const noexcept { return header_; } + auto& header() noexcept { return header_; } + + char* query_name() noexcept { return reinterpret_cast(&header_) + sizeof(Header); } + + int hostname_size() const noexcept { return hostname_size_; } + + Question& question() noexcept + { + return *reinterpret_cast(query_name() + hostname_size_); + } + + int size() const noexcept { return sizeof(Header) + hostname_size_ + sizeof(Question); } + + private: + // bookkeeping + int64_t hostname_size_; + + Header header_; +}; +#pragma pack(pop) + +/// Convert strings like www.google.com to 3www6google3com, where numbers are bytes that follow +std::string dns_format(const std::string& host) +{ + std::size_t pos = 0; + std::stringstream out; + std::string_view view{host}; + while ((pos = view.find_first_of('.'), pos) != std::string_view::npos) { + out << static_cast(pos) << view.substr(0, pos); + view.remove_prefix(pos + 1); + } + out << static_cast(view.size()) << view; + return out.str(); +} + +toolbox::net::IpAddrV4 TOOLBOX_API get_host_by_name(const std::string& nameserver, + const std::string& hostname, + DnsRequest::Type query_type) +{ + sockaddr_in dest; + int sckt = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); //UDP packet for DNS queries + dest.sin_family = AF_INET; + dest.sin_port = htons(53); + dest.sin_addr.s_addr = inet_addr(nameserver.c_str()); + + //Set the DNS structure to standard queries + char send_buffer[2048]; + Dns* dns = new (send_buffer) Dns{hostname, query_type}; + + //point to the query portion + int query_name_size = dns->hostname_size(); + + if (sendto(sckt, reinterpret_cast(&dns->header()), dns->size(), 0, + (struct sockaddr*)&dest, sizeof(dest)) + < 0) { + throw std::runtime_error{"Error sending dns request"}; + } + + // TODO: Split request / response through reactor to make it asynchronous. + // use id field to track them if required. + + //Receive the answer + int dest_size = sizeof dest; + char recv_buffer[2048]; + + if (recvfrom(sckt, recv_buffer, sizeof(recv_buffer), 0, (sockaddr*)&dest, + (socklen_t*)&dest_size) + < 0) { + throw std::runtime_error{"Error receiving dns response"}; + } + + // Adjust dns to struct + dns = reinterpret_cast(&recv_buffer); + + //move ahead of the dns header and the query field + char* reader = &recv_buffer[sizeof(Dns::Header) + query_name_size + sizeof(Dns::Question)]; + + //Start reading answers + for (int i = 0; i < dns->header().ans_count(); i++) { + if (*reinterpret_cast(reader) >= 192) // Name is in the header (skip) + { + reader += 2; + } else { + throw std::runtime_error("Implement in answer full name decoding"); + } + + const auto& r_data = *reinterpret_cast(reader); + reader = reader + sizeof(Dns::RData); + + assert(r_data._class() == 1); + + switch (r_data.type()) { + case Dns::RData::Type::A_RECORD: { + const auto& bytes = *reinterpret_cast(reader); + return boost::asio::ip::make_address_v4(bytes); + } + default: { //Skip all other records + reader = reader + r_data.length(); + } + } + } + throw std::runtime_error{"Could not resolve addr"}; +} + +} // namespace net +} // namespace toolbox diff --git a/toolbox/net/Resolver2.hpp b/toolbox/net/Resolver2.hpp new file mode 100644 index 000000000..2bf2dba20 --- /dev/null +++ b/toolbox/net/Resolver2.hpp @@ -0,0 +1,60 @@ +// The Reactive C++ Toolbox. +// Copyright (C) 2013-2019 Swirly Cloud Limited +// Copyright (C) 2019 Reactive Markets Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef TOOLBOX_NET_RESOLVER2_HPP +#define TOOLBOX_NET_RESOLVER2_HPP + +#include +#include + +namespace toolbox { +inline namespace net { + +/// Convert strings like www.google.com to 3www6google3com, where numbers are bytes that follow +TOOLBOX_API char* to_dns(const std::string& host, char* buff); + +class TOOLBOX_API DnsServers { + public: + using Servers = std::vector; + + explicit DnsServers(std::istream& resolv_conf, + std::vector additional_nameservers = {}); + + const Servers& servers() const noexcept; + const std::string& server() const; + + private: + Servers servers_; +}; + +struct DnsRequest { + enum Type { + A = 1, // Ipv4 address + NS = 2, // Nameserver + CNAME = 5, // Canonical name + SOA = 6, // Start of authority zone + PTR = 12, // Domain name pointer + MX = 15, // Mail server + }; +}; + +IpAddrV4 TOOLBOX_API get_host_by_name(const std::string& nameserver, const std::string& hostname, + DnsRequest::Type query_type); + +} // namespace net +} // namespace toolbox + +#endif // TOOLBOX_NET_RESOLVER_HPP diff --git a/toolbox/net/Resolver2.ut.cpp b/toolbox/net/Resolver2.ut.cpp new file mode 100644 index 000000000..6809c4476 --- /dev/null +++ b/toolbox/net/Resolver2.ut.cpp @@ -0,0 +1,54 @@ +// The Reactive C++ Toolbox. +// Copyright (C) 2013-2019 Swirly Cloud Limited +// Copyright (C) 2019 Reactive Markets Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "Resolver2.hpp" + +#include + +#include + +using namespace std; +using namespace toolbox; + +BOOST_AUTO_TEST_SUITE(ResolverSuite2) + +BOOST_AUTO_TEST_CASE(DnsServersConf) +{ + + stringstream conf{"# Comment\n" + "nameserver 1.1.1.1\n"}; + + DnsServers dns_servers{conf}; + + BOOST_TEST(dns_servers.server() == "1.1.1.1"); +} + +BOOST_AUTO_TEST_CASE(DnsNameEnconding) +{ + std::array enc; + enc.fill(0xcc); + + const std::string host{"www.reactivemarkets.com"}; + to_dns(host, enc.data()); + + stringstream ss; + ss << hex_dump(enc.data(), host.size() + 3, + hex_dump::Mode::NON_PRINTABLE); // +3 (0x03 + terminator + first 0xcc) + + BOOST_TEST(ss.str() == " 0x03 w w w 0x0f r e a c t i v e m a r k e t s 0x03 c o m 0x00 0xcc"); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/toolbox/util/Debug.hpp b/toolbox/util/Debug.hpp new file mode 100644 index 000000000..80c1e70d6 --- /dev/null +++ b/toolbox/util/Debug.hpp @@ -0,0 +1,68 @@ +// The Reactive C++ Toolbox. +// Copyright (C) 2019 Reactive Markets Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef TOOLBOX_UTIL_DEBUG_HPP +#define TOOLBOX_UTIL_DEBUG_HPP + +#include + +#include +#include + +namespace toolbox { +inline namespace util { + +class hex_dump { + public: + enum class Mode { ALL = 0, NON_PRINTABLE }; + + template + explicit hex_dump(const Type* obj, int size = sizeof(Type), Mode mode = Mode::ALL) + : obj_{reinterpret_cast(obj)} + , size_{size} + , mode_{mode} + { + } + + friend std::ostream& operator<<(std::ostream& out, const hex_dump& dump) + { + if (dump.obj_) { + auto begin = dump.obj_; + const auto end = begin + dump.size_; + out << std::hex << std::setfill('0'); + while (begin != end) { + const int ch = +*begin; + if (isprint(ch) && dump.mode_ == Mode::NON_PRINTABLE) { + out << ' ' << static_cast(ch); + } else { + out << " 0x" << std::setw(2) << +ch; + } + ++begin; + } + out << std::dec << std::setfill(' '); + } + return out; + } + + private: + const uint8_t* obj_; + int size_; + Mode mode_; +}; + +} // namespace util +} // namespace toolbox + +#endif // TOOLBOX_UTIL_DEBUG_HPP diff --git a/toolbox/util/Debug.ut.cpp b/toolbox/util/Debug.ut.cpp new file mode 100644 index 000000000..60c4d2f48 --- /dev/null +++ b/toolbox/util/Debug.ut.cpp @@ -0,0 +1,43 @@ +// The Reactive C++ Toolbox. +// Copyright (C) 2013-2019 Swirly Cloud Limited +// Copyright (C) 2019 Reactive Markets Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "Debug.hpp" + +#include + +using namespace std; +using namespace toolbox; + +BOOST_AUTO_TEST_SUITE(DebugSuite) + +BOOST_AUTO_TEST_CASE(DumpCase) +{ + std::string data{"12345"}; + { + stringstream ss; + ss << hex_dump(data.c_str(), data.size() + 1); + + BOOST_TEST(ss.str() == " 0x31 0x32 0x33 0x34 0x35 0x00"); + } + { + stringstream ss; + ss << hex_dump(data.c_str(), data.size() + 1, hex_dump::Mode::NON_PRINTABLE); + + BOOST_TEST(ss.str() == " 1 2 3 4 5 0x00"); + } +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/toolbox/util/Stream.cpp b/toolbox/util/Stream.cpp index bbb0ec8d0..b3abdd580 100644 --- a/toolbox/util/Stream.cpp +++ b/toolbox/util/Stream.cpp @@ -16,6 +16,8 @@ #include "Stream.hpp" +#include + namespace toolbox { inline namespace util { using namespace std; @@ -29,5 +31,12 @@ void reset(ostream& os) noexcept os.width(0); } +std::stringstream wrap_buffer(char* buf, int size) +{ + std::stringstream stream; + stream.rdbuf()->pubsetbuf(buf, size); + return stream; +} + } // namespace util } // namespace toolbox diff --git a/toolbox/util/Stream.hpp b/toolbox/util/Stream.hpp index 43fd606de..d26ce0ead 100644 --- a/toolbox/util/Stream.hpp +++ b/toolbox/util/Stream.hpp @@ -102,6 +102,9 @@ void join(std::ostream& os, const ArgT& arg, const ArgsT&... args) (..., [&os](const auto& arg) { os << DelimT << arg; }(args)); } +/// Adapt char * into stream interface +TOOLBOX_API std::stringstream wrap_buffer(char* buf, int size); + } // namespace util } // namespace toolbox @@ -112,6 +115,7 @@ ostream_joiner& operator<<(ostream_joiner& osj, const ValueT& value) osj = value; return osj; } + } // namespace std::experimental #endif // TOOLBOX_UTIL_STREAM_HPP diff --git a/toolbox/util/Stream.ut.cpp b/toolbox/util/Stream.ut.cpp index 393d6c9e4..f780ca670 100644 --- a/toolbox/util/Stream.ut.cpp +++ b/toolbox/util/Stream.ut.cpp @@ -57,4 +57,14 @@ BOOST_AUTO_TEST_CASE(OStreamJoinerCase) BOOST_TEST(ss.str() == "foo,bar,baz"); } +BOOST_AUTO_TEST_CASE(StreamWrapping) +{ + char arr[20]; + auto ss = util::wrap_buffer(arr, sizeof(arr)); + ss << "string" << 1.2 << '\0'; + + BOOST_TEST(!strcmp(arr, "string1.2")); + //TODO: test limit, overflow, empty read etc; +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/toolbox/util/Traits.ut.cpp b/toolbox/util/Traits.ut.cpp index 48cf645d5..1d9407f17 100644 --- a/toolbox/util/Traits.ut.cpp +++ b/toolbox/util/Traits.ut.cpp @@ -156,7 +156,7 @@ BOOST_AUTO_TEST_CASE(TraitsConstLambdaCase) BOOST_AUTO_TEST_CASE(TraitsConstNoexceptLambdaCase) { - const auto fn = [](short, int, long) noexcept->double { return 0.0; }; + const auto fn = [](short, int, long) noexcept -> double { return 0.0; }; using Traits = FunctionTraits; using Tuple = Traits::Pack;