diff --git a/.circleci/config.yml b/.circleci/config.yml index 8d78c1ed8..fe758e547 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -35,7 +35,7 @@ jobs: -DCMAKE_BUILD_TYPE=DEBUG \ -DCMAKE_C_COMPILER=clang-7 \ -DCMAKE_CXX_COMPILER=clang++-7 - VERBOSE=1 make -j2 all test + CTEST_OUTPUT_ON_FAILURE=1 VERBOSE=1 make -j2 all test test_clang_release: docker: - image: registry.gitlab.com/reactivemarkets/public-registry/toolchain-cpp:debian @@ -52,7 +52,7 @@ jobs: -DCMAKE_BUILD_TYPE=RELEASE \ -DCMAKE_C_COMPILER=clang-7 \ -DCMAKE_CXX_COMPILER=clang++-7 - VERBOSE=1 make -j2 all test + CTEST_OUTPUT_ON_FAILURE=1 VERBOSE=1 make -j2 all test test_gcc_debug: docker: - image: registry.gitlab.com/reactivemarkets/public-registry/toolchain-cpp:debian @@ -69,7 +69,7 @@ jobs: -DCMAKE_BUILD_TYPE=DEBUG \ -DCMAKE_C_COMPILER=gcc-8 \ -DCMAKE_CXX_COMPILER=g++-8 - VERBOSE=1 make -j2 all test + CTEST_OUTPUT_ON_FAILURE=1 VERBOSE=1 make -j2 all test test_gcc_release: docker: - image: registry.gitlab.com/reactivemarkets/public-registry/toolchain-cpp:debian @@ -86,7 +86,7 @@ jobs: -DCMAKE_BUILD_TYPE=RELEASE \ -DCMAKE_C_COMPILER=gcc-8 \ -DCMAKE_CXX_COMPILER=g++-8 - VERBOSE=1 make -j2 all test + CTEST_OUTPUT_ON_FAILURE=1 VERBOSE=1 make -j2 all test build_doc: docker: - image: registry.gitlab.com/reactivemarkets/public-registry/toolchain-cpp:debian 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..a0320e94a --- /dev/null +++ b/example/Dns.cpp @@ -0,0 +1,134 @@ +// 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. + +#include +#include +#include +#include + +#include +#include + +using namespace std; +using namespace toolbox; +using namespace toolbox::net; + +class DnsRequestHandler { + public: + DnsRequestHandler(Reactor& reactor, IoSock& sock, const std::string& nameserver) + : reactor_{reactor} + , sock_{sock} + , handle_{reactor.subscribe(*sock_, EventIn, bind<&DnsRequestHandler::on_request>(this))} + , ep_{UdpProtocol::v4(), 63} + , nameserver_{nameserver} + { + } + + void on_request(CyclTime now, int fd, unsigned events) + { + // TODO: Replace with protocol or allow bind to wrapped message types (MutableBuffer?) + char size{0}; + os::recv(fd, &size, 1, 0); + char hostname_buffer[64]; + os::recv(fd, hostname_buffer, size, 0); + string_view hostname{hostname_buffer, static_cast(size)}; + TOOLBOX_INFO << "Requesting " << hostname; + auto udp_fd = send_dns_request(nameserver_, string{hostname}, DnsRequest::A); + TOOLBOX_INFO << "Subscribing to response"; + response_ + = reactor_.subscribe(udp_fd, EventIn, bind<&DnsRequestHandler::on_response>(this)); + } + + void on_response(CyclTime now, int fd, unsigned events) + { + TOOLBOX_INFO << "Processing response"; + std::cout << get_dns_request(fd, ep_) << '\n'; + kill(getpid(), SIGTERM); + } + + private: + Reactor& reactor_; + IoSock& sock_; + Reactor::Handle handle_; + Reactor::Handle response_; + UdpEndpoint ep_; + const std::string& nameserver_; +}; + +int main(int argc, char* argv[]) +{ + std::ios::sync_with_stdio(false); + int ret = 1; + + 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(); + } + + EpollReactor reactor{1024}; + Resolver resolver; + // Start service threads. + pthread_setname_np(pthread_self(), "main"); + + auto socks = socketpair(UnixStreamProtocol{}); + DnsRequestHandler h{reactor, socks.second, nameserver}; + + ReactorRunner reactor_runner{reactor, "reactor"s}; + + char request_buf[40]; + request_buf[0] = hostname.size(); + memcpy(&request_buf[1], hostname.data(), hostname.size()); + socks.first.send(request_buf, hostname.size() + 1, 0); + + // Wait for termination. + SigWait sig_wait; + for (;;) { + switch (const auto sig = sig_wait()) { + case SIGHUP: + TOOLBOX_INFO << "received SIGHUP"; + continue; + case SIGINT: + TOOLBOX_INFO << "received SIGINT"; + break; + case SIGTERM: + TOOLBOX_INFO << "received SIGTERM"; + break; + default: + TOOLBOX_INFO << "received signal: " << sig; + continue; + } + break; + } + ret = 0; + } catch (const std::exception& e) { + TOOLBOX_ERROR << "exception: " << e.what(); + } + return ret; +} diff --git a/example/EchoClnt.cpp b/example/EchoClnt.cpp index 5b2e71117..a11d1e6dc 100644 --- a/example/EchoClnt.cpp +++ b/example/EchoClnt.cpp @@ -34,9 +34,8 @@ class EchoConn { using AutoUnlinkOption = boost::intrusive::link_mode; public: - EchoConn(CyclTime now, Reactor& r, IoSock&& sock, const StreamEndpoint& ep) + EchoConn(CyclTime now, Reactor& r, IoSock&& sock) : sock_{move(sock)} - , ep_{ep} { sub_ = r.subscribe(sock_.get(), EventIn, bind<&EchoConn::on_input>(this)); tmr_ = r.timer(now.mono_time(), PingInterval, Priority::Low, @@ -91,7 +90,6 @@ class EchoConn { } } IoSock sock_; - const StreamEndpoint ep_; Reactor::Handle sub_; Buffer buf_; Timer tmr_; @@ -137,7 +135,7 @@ class EchoClnt : public StreamConnector { inprogress_ = false; // High performance TCP servers could use a custom allocator. - auto* const conn = new EchoConn{now, reactor_, move(sock), ep}; + auto* const conn = new EchoConn{now, reactor_, move(sock)}; conn_list_.push_back(*conn); } void on_sock_connect_error(CyclTime now, const std::exception& e) 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/Error.hpp b/toolbox/net/Error.hpp index 0572a7dac..37bca7359 100644 --- a/toolbox/net/Error.hpp +++ b/toolbox/net/Error.hpp @@ -17,13 +17,15 @@ #ifndef TOOLBOX_NET_ERROR_HPP #define TOOLBOX_NET_ERROR_HPP +#include + #include namespace toolbox { inline namespace net { -const std::error_category& gai_error_category() noexcept; -std::error_code make_gai_error_code(int err) noexcept; +TOOLBOX_API const std::error_category& gai_error_category() noexcept; +TOOLBOX_API std::error_code make_gai_error_code(int err) noexcept; } // namespace net } // namespace toolbox diff --git a/toolbox/net/Resolver2.cpp b/toolbox/net/Resolver2.cpp new file mode 100644 index 000000000..847000700 --- /dev/null +++ b/toolbox/net/Resolver2.cpp @@ -0,0 +1,285 @@ +// 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. + +#include "Resolver2.hpp" + +#include "Endpoint.hpp" +#include "Socket.hpp" + +#include + +#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_{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} + { + } + + uint16_t id() const noexcept { return ntohs(id_); } + 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_); } + + char* data() noexcept { return reinterpret_cast(&length_) + sizeof(length_); } + const char* data() const noexcept + { + return reinterpret_cast(&length_) + sizeof(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); } + + static constexpr int offset() { return sizeof(hostname_size_); } + + void bookkeep() noexcept { hostname_size_ = strlen(query_name()) + 1; } + + char* reader() noexcept { return reinterpret_cast(&header_) + size(); } + const char* reader() const noexcept { return reader(); } + + private: + // bookkeeping + int16_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(); +} + +int send_dns_request(const std::string& nameserver, const std::string& hostname, + DnsRequest::Type query_type) +{ + int sckt = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); //UDP packet for DNS queries + sockaddr_in dest; + 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 + unsigned char send_buffer[1024]; + Dns* dns = new (send_buffer) Dns{hostname, query_type}; + + os::sendto(sckt, &dns->header(), dns->size(), 0, reinterpret_cast(dest), + sizeof(sockaddr_in)); + + return sckt; +} + +toolbox::net::IpAddrV4 get_dns_request(int fd, UdpEndpoint& endpoint) +{ + unsigned char recv_buffer[2048]; + + os::recvfrom(fd, recv_buffer + Dns::offset(), sizeof(recv_buffer) - Dns::offset(), 0, endpoint); + + // Adjust dns to struct + auto& dns = *reinterpret_cast(&recv_buffer); + dns.bookkeep(); + + char* reader = dns.reader(); + + //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 full name decoding"); + } + + const auto& r_data = *reinterpret_cast(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(r_data.data()); + return boost::asio::ip::make_address_v4(bytes); + } + default: { //Skip all other records + 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..635bf2074 --- /dev/null +++ b/toolbox/net/Resolver2.hpp @@ -0,0 +1,65 @@ +// 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_NET_RESOLVER2_HPP +#define TOOLBOX_NET_RESOLVER2_HPP + +#include +#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); + +int TOOLBOX_API send_dns_request(const std::string& nameserver, const std::string& hostname, + DnsRequest::Type query_type); + +IpAddrV4 TOOLBOX_API get_dns_request(int fd, UdpEndpoint& endpoint); + +} // 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..40e5923fb --- /dev/null +++ b/toolbox/net/Resolver2.ut.cpp @@ -0,0 +1,53 @@ +// 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. + +#include "Resolver2.hpp" + +#include + +#include + +using namespace std; +using namespace toolbox; + +BOOST_AUTO_TEST_SUITE(Resolver2Suite) + +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..ce03423ef --- /dev/null +++ b/toolbox/util/Debug.ut.cpp @@ -0,0 +1,42 @@ +// 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. + +#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()