mirror of
https://github.com/kiwix/libkiwix.git
synced 2026-02-24 10:18:07 -05:00
Compare commits
11 Commits
main
...
support_fo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34fc831c13 | ||
|
|
47f1a61443 | ||
|
|
a3b246fd6d | ||
|
|
75ff043476 | ||
|
|
0f7458c693 | ||
|
|
add7c4cc3a | ||
|
|
fcdc3a7eaf | ||
|
|
db3705dd23 | ||
|
|
3b34130cfc | ||
|
|
b773e93a95 | ||
|
|
e481164258 |
@@ -18,7 +18,7 @@ import os
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = 'libkiwix'
|
||||
copyright = '2022, libkiwix-team'
|
||||
copyright = '2026, libkiwix-team'
|
||||
author = 'libkiwix-team'
|
||||
|
||||
|
||||
|
||||
@@ -14,12 +14,43 @@
|
||||
#endif
|
||||
|
||||
|
||||
namespace kiwix {
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
/**
|
||||
* `IpMode` is used to [configure](@ref Server::setIpMode()) a `Server` object
|
||||
* to listen for incoming connections not on a single IP address but on all
|
||||
* addresses of the specified family.
|
||||
*/
|
||||
enum class IpMode
|
||||
{
|
||||
/**
|
||||
* Listen on all IPv4 addresses.
|
||||
*/
|
||||
IPV4,
|
||||
|
||||
/**
|
||||
* Listen on all IPv6 addresses.
|
||||
*/
|
||||
IPV6,
|
||||
|
||||
/**
|
||||
* Listen on all available addresses.
|
||||
*/
|
||||
ALL,
|
||||
|
||||
/**
|
||||
* `IpMode::AUTO` (which is the default) must be used when an explicit
|
||||
* (non-empty) IP address for listening is provided via
|
||||
* `Server::setAddress()`. If no such address is enforced, then
|
||||
* `IpMode::AUTO` is equivalent to `IpMode::ALL`.
|
||||
*/
|
||||
AUTO
|
||||
};
|
||||
|
||||
enum class IpMode { IPV4, IPV6, ALL, AUTO }; // AUTO: Server decides the protocol
|
||||
typedef zim::size_type size_type;
|
||||
typedef zim::offset_type offset_type;
|
||||
|
||||
}
|
||||
} // namespace kiwix
|
||||
|
||||
#endif //_KIWIX_COMMON_H_
|
||||
|
||||
@@ -42,7 +42,7 @@ namespace kiwix
|
||||
virtual ~Server();
|
||||
|
||||
/**
|
||||
* Serve the content.
|
||||
* Start serving the content.
|
||||
*/
|
||||
bool start();
|
||||
|
||||
@@ -51,9 +51,27 @@ namespace kiwix
|
||||
*/
|
||||
void stop();
|
||||
|
||||
/**
|
||||
* Set the path of the root URL served by this server instance.
|
||||
*/
|
||||
void setRoot(const std::string& root);
|
||||
|
||||
/**
|
||||
* Set the IP address on which to listen for incoming connections.
|
||||
*
|
||||
* Specifying a non-empty IP address requires that the IpMode is
|
||||
* [set](@ref setIpMode()) to `IpMode::AUTO` (which is the default).
|
||||
* Otherwise, [starting](@ref start()) the server will fail.
|
||||
*/
|
||||
void setAddress(const std::string& addr);
|
||||
|
||||
/**
|
||||
* Set the port on which to listen for incoming connections.
|
||||
*
|
||||
* Default port is 80, but using it requires special privileges.
|
||||
*/
|
||||
void setPort(int port) { m_port = port; }
|
||||
|
||||
void setNbThreads(int threads) { m_nbThreads = threads; }
|
||||
void setMultiZimSearchLimit(unsigned int limit) { m_multizimSearchLimit = limit; }
|
||||
void setIpConnectionLimit(int limit) { m_ipConnectionLimit = limit; }
|
||||
@@ -65,10 +83,50 @@ namespace kiwix
|
||||
{ m_blockExternalLinks = blockExternalLinks; }
|
||||
void setCatalogOnlyMode(bool enable) { m_catalogOnlyMode = enable; }
|
||||
void setContentServerUrl(std::string url) { m_contentServerUrl = url; }
|
||||
|
||||
/**
|
||||
* Listen for incoming connections on all IP addresses of the specified
|
||||
* IP protocol family.
|
||||
*/
|
||||
void setIpMode(IpMode mode) { m_ipMode = mode; }
|
||||
|
||||
/**
|
||||
* Get the port on which the server listens for incoming connections
|
||||
*/
|
||||
int getPort() const;
|
||||
|
||||
/**
|
||||
* Get the IPv4 and/or IPv6 address(es) on which the server can be
|
||||
* contacted.
|
||||
*
|
||||
* The server may actually be listening on other IP addresses as well
|
||||
* (see `setIpMode()`). The IP address(es) returned by this method
|
||||
* represent the best-guess public IP address(es) accessible by the
|
||||
* broadest set of clients.
|
||||
*/
|
||||
IpAddress getAddress() const;
|
||||
|
||||
/**
|
||||
* Get the effective IpMode used by this server.
|
||||
*
|
||||
* The returned value may be different from the one configured via
|
||||
* `setIpMode()`, since the server is expected to adjust to the
|
||||
* constraints of the environment (e.g. `IpMode::ALL` can be converted
|
||||
* to `IpMode::IPV4` on a system that does not support IPv6).
|
||||
*/
|
||||
IpMode getIpMode() const;
|
||||
|
||||
/**
|
||||
* Get the list of HTTP URLs through which the server can be contacted.
|
||||
*
|
||||
* Each URL is composed of an IP address and optional port and includes
|
||||
* the [root](@ref setRoot()) path component.
|
||||
*
|
||||
* In the current implementation at most 2 URLs may be returned - one
|
||||
* for IPv4 and another for IPv6 protocol (whichever is available), as
|
||||
* returned by `getAddress()`. Note, however, that the server may be
|
||||
* also accessible via other IP-addresses/URLs (see `setIpMode()`).
|
||||
*/
|
||||
std::vector<std::string> getServerAccessUrls() const;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -29,10 +29,13 @@
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
/**
|
||||
* An IPv4 and/or IPv6 address.
|
||||
*/
|
||||
struct IpAddress
|
||||
{
|
||||
std::string addr; // IPv4 address
|
||||
std::string addr6; // IPv6 address
|
||||
std::string addr; /**< IPv4 address */
|
||||
std::string addr6; /**< IPv6 address */
|
||||
};
|
||||
|
||||
typedef std::pair<std::string, std::string> LangNameCodePair;
|
||||
@@ -263,7 +266,7 @@ std::string getLanguageSelfName(const std::string& lang);
|
||||
* Slugifies the filename by converting any characters reserved by the operating
|
||||
* system to '_'. Note filename is only the file name and not a path.
|
||||
*
|
||||
* @param filename Valid UTF-8 encoded file name string.
|
||||
* @param filename Valid UTF-8 encoded file name string.
|
||||
* @return slugified string.
|
||||
*/
|
||||
std::string getSlugifiedFileName(const std::string& filename);
|
||||
|
||||
@@ -85,6 +85,10 @@ namespace kiwix {
|
||||
namespace
|
||||
{
|
||||
|
||||
void error(const std::string& msg) {
|
||||
throw std::runtime_error(msg);
|
||||
}
|
||||
|
||||
bool ipAvailable(const std::string addr)
|
||||
{
|
||||
auto interfaces = kiwix::getNetworkInterfacesIPv4Or6();
|
||||
@@ -99,6 +103,84 @@ bool ipAvailable(const std::string addr)
|
||||
return false;
|
||||
}
|
||||
|
||||
class InSockAddr
|
||||
{
|
||||
private: // data
|
||||
struct sockaddr_in v4 = {0};
|
||||
struct sockaddr_in6 v6 = {0};
|
||||
|
||||
public: // functions
|
||||
explicit InSockAddr(int port)
|
||||
{
|
||||
v4.sin_family = AF_INET;
|
||||
v4.sin_port = htons(port);
|
||||
v6.sin6_family = AF_INET6;
|
||||
v6.sin6_port = htons(port);
|
||||
}
|
||||
|
||||
IpAddress setAnyAddress(IpMode ipMode)
|
||||
{
|
||||
v6.sin6_addr = in6addr_any;
|
||||
v4.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||
|
||||
IpAddress a = kiwix::getBestPublicIps();
|
||||
if (ipMode == IpMode::IPV6) {
|
||||
a.addr = "";
|
||||
} else if (ipMode == IpMode::IPV4) {
|
||||
a.addr6 = "";
|
||||
}
|
||||
|
||||
return a;
|
||||
}
|
||||
|
||||
IpMode setAddress(const IpAddress& a)
|
||||
{
|
||||
const std::string addrStr = !a.addr.empty() ? a.addr : a.addr6;
|
||||
|
||||
const int r1 = inet_pton(AF_INET, a.addr.c_str(), &v4.sin_addr.s_addr);
|
||||
const int r2 = inet_pton(AF_INET6, a.addr6.c_str(), &v6.sin6_addr.s6_addr);
|
||||
|
||||
if ( r1 != 1 && r2 != 1 ) {
|
||||
error("invalid IP address: " + addrStr);
|
||||
}
|
||||
|
||||
if ( !ipAvailable(addrStr) ) {
|
||||
error("IP address is not available on this system: " + addrStr);
|
||||
}
|
||||
|
||||
return !a.addr.empty() ? IpMode::IPV4 : IpMode::IPV6;
|
||||
}
|
||||
|
||||
struct sockaddr* sockaddr(IpMode ipMode) const
|
||||
{
|
||||
return (ipMode==IpMode::ALL || ipMode==IpMode::IPV6)
|
||||
? (struct sockaddr*)&v6
|
||||
: (struct sockaddr*)&v4;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
int getMHDFlags(IpMode ipMode, bool verbose)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
int flags = MHD_USE_SELECT_INTERNALLY;
|
||||
#else
|
||||
int flags = MHD_USE_POLL_INTERNALLY;
|
||||
#endif
|
||||
|
||||
if (ipMode == IpMode::ALL) {
|
||||
flags |= MHD_USE_DUAL_STACK;
|
||||
} else if (ipMode == IpMode::IPV6) {
|
||||
flags |= MHD_USE_IPv6;
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
flags |= MHD_USE_DEBUG;
|
||||
}
|
||||
|
||||
return flags;
|
||||
}
|
||||
|
||||
std::string
|
||||
fullURL2LocalURL(const std::string& fullUrl, const std::string& rootLocation)
|
||||
{
|
||||
@@ -450,81 +532,61 @@ InternalServer::InternalServer(LibraryPtr library,
|
||||
|
||||
InternalServer::~InternalServer() = default;
|
||||
|
||||
bool InternalServer::start() {
|
||||
#ifdef _WIN32
|
||||
int flags = MHD_USE_SELECT_INTERNALLY;
|
||||
#else
|
||||
int flags = MHD_USE_POLL_INTERNALLY;
|
||||
#endif
|
||||
if (m_verbose.load())
|
||||
flags |= MHD_USE_DEBUG;
|
||||
|
||||
|
||||
struct sockaddr_in sockAddr4={0};
|
||||
sockAddr4.sin_family = AF_INET;
|
||||
sockAddr4.sin_port = htons(m_port);
|
||||
struct sockaddr_in6 sockAddr6={0};
|
||||
sockAddr6.sin6_family = AF_INET6;
|
||||
sockAddr6.sin6_port = htons(m_port);
|
||||
struct MHD_Daemon* InternalServer::startMHD(int flags,
|
||||
struct sockaddr* sockaddr)
|
||||
{
|
||||
return MHD_start_daemon(flags,
|
||||
m_port,
|
||||
NULL,
|
||||
NULL,
|
||||
&staticHandlerCallback,
|
||||
this,
|
||||
MHD_OPTION_SOCK_ADDR, sockaddr,
|
||||
MHD_OPTION_THREAD_POOL_SIZE, m_nbThreads,
|
||||
MHD_OPTION_PER_IP_CONNECTION_LIMIT, m_ipConnectionLimit,
|
||||
MHD_OPTION_END);
|
||||
}
|
||||
|
||||
void InternalServer::startMHD() {
|
||||
InSockAddr inSockAddr(m_port);
|
||||
if (m_addr.addr.empty() && m_addr.addr6.empty()) { // No ip address provided
|
||||
if (m_ipMode == IpMode::AUTO) m_ipMode = IpMode::ALL;
|
||||
sockAddr6.sin6_addr = in6addr_any;
|
||||
sockAddr4.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||
IpAddress bestIps = kiwix::getBestPublicIps();
|
||||
if (m_ipMode == IpMode::IPV4 || m_ipMode == IpMode::ALL) m_addr.addr = bestIps.addr;
|
||||
if (m_ipMode == IpMode::IPV6 || m_ipMode == IpMode::ALL) m_addr.addr6 = bestIps.addr6;
|
||||
m_addr = inSockAddr.setAnyAddress(m_ipMode);
|
||||
} else {
|
||||
const std::string addr = !m_addr.addr.empty() ? m_addr.addr : m_addr.addr6;
|
||||
|
||||
if (m_ipMode != kiwix::IpMode::AUTO) {
|
||||
std::cerr << "ERROR: When an IP address is provided the IP mode must not be set" << std::endl;
|
||||
return false;
|
||||
error("When an IP address is provided the IP mode must not be set");
|
||||
}
|
||||
|
||||
bool validV4 = inet_pton(AF_INET, m_addr.addr.c_str(), &(sockAddr4.sin_addr.s_addr)) == 1;
|
||||
bool validV6 = inet_pton(AF_INET6, m_addr.addr6.c_str(), &(sockAddr6.sin6_addr.s6_addr)) == 1;
|
||||
|
||||
if (!validV4 && !validV6) {
|
||||
std::cerr << "ERROR: invalid IP address: " << addr << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ipAvailable(addr)) {
|
||||
std::cerr << "ERROR: IP address is not available on this system: " << addr << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
m_ipMode = !m_addr.addr.empty() ? IpMode::IPV4 : IpMode::IPV6;
|
||||
m_ipMode = inSockAddr.setAddress(m_addr);
|
||||
}
|
||||
|
||||
if (m_ipMode == IpMode::ALL) {
|
||||
flags|=MHD_USE_DUAL_STACK;
|
||||
} else if (m_ipMode == IpMode::IPV6) {
|
||||
flags|=MHD_USE_IPv6;
|
||||
const int flags = getMHDFlags(m_ipMode, m_verbose.load());
|
||||
mp_daemon = startMHD(flags, inSockAddr.sockaddr(m_ipMode));
|
||||
if (mp_daemon == nullptr && m_ipMode == IpMode::ALL) {
|
||||
// MHD_USE_DUAL_STACK (set in IpMode::ALL case) fails on systems with IPv6
|
||||
// disabled. Let's retry in IPv4-only mode.
|
||||
m_ipMode = IpMode::IPV4;
|
||||
m_addr.addr6 = "";
|
||||
const int flags = getMHDFlags(m_ipMode, m_verbose.load());
|
||||
mp_daemon = startMHD(flags, inSockAddr.sockaddr(m_ipMode));
|
||||
}
|
||||
|
||||
struct sockaddr* sockaddr = (m_ipMode==IpMode::ALL || m_ipMode==IpMode::IPV6)
|
||||
? (struct sockaddr*)&sockAddr6
|
||||
: (struct sockaddr*)&sockAddr4;
|
||||
|
||||
mp_daemon = MHD_start_daemon(flags,
|
||||
m_port,
|
||||
NULL,
|
||||
NULL,
|
||||
&staticHandlerCallback,
|
||||
this,
|
||||
MHD_OPTION_SOCK_ADDR, sockaddr,
|
||||
MHD_OPTION_THREAD_POOL_SIZE, m_nbThreads,
|
||||
MHD_OPTION_PER_IP_CONNECTION_LIMIT, m_ipConnectionLimit,
|
||||
MHD_OPTION_END);
|
||||
if (mp_daemon == nullptr) {
|
||||
std::cerr << "Unable to instantiate the HTTP daemon. The port " << m_port
|
||||
<< " is maybe already occupied or need more permissions to be open. "
|
||||
"Please try as root or with a port number higher or equal to 1024."
|
||||
<< std::endl;
|
||||
error("Unable to instantiate the HTTP daemon. "
|
||||
"The port " + kiwix::to_string(m_port) + " is maybe already occupied"
|
||||
" or need more permissions to be open. "
|
||||
"Please try as root or with a port number higher or equal to 1024.");
|
||||
}
|
||||
}
|
||||
|
||||
bool InternalServer::start() {
|
||||
try {
|
||||
startMHD();
|
||||
} catch (const std::runtime_error& err ) {
|
||||
std::cerr << "ERROR: " << err.what() << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto server_start_time = std::chrono::system_clock::now().time_since_epoch();
|
||||
m_server_id = kiwix::to_string(server_start_time.count());
|
||||
return true;
|
||||
|
||||
@@ -126,6 +126,8 @@ class InternalServer {
|
||||
IpMode getIpMode() const { return m_ipMode; }
|
||||
|
||||
private: // functions
|
||||
void startMHD();
|
||||
struct MHD_Daemon* startMHD(int flags, struct sockaddr* sockaddr);
|
||||
std::unique_ptr<Response> handle_request(const RequestContext& request);
|
||||
std::unique_ptr<Response> build_redirect(const std::string& bookName, const zim::Item& item) const;
|
||||
std::unique_ptr<Response> build_homepage(const RequestContext& request);
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
#include "../include/library.h"
|
||||
#include "../include/manager.h"
|
||||
#include "testing_tools.h"
|
||||
using namespace kiwix::testing;
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
namespace
|
||||
@@ -58,27 +61,6 @@ class NameMapperTest : public ::testing::Test {
|
||||
std::shared_ptr<kiwix::Library> lib;
|
||||
};
|
||||
|
||||
class CapturedStderr
|
||||
{
|
||||
std::ostringstream buffer;
|
||||
std::streambuf* const sbuf;
|
||||
public:
|
||||
CapturedStderr()
|
||||
: sbuf(std::cerr.rdbuf())
|
||||
{
|
||||
std::cerr.rdbuf(buffer.rdbuf());
|
||||
}
|
||||
|
||||
CapturedStderr(const CapturedStderr&) = delete;
|
||||
|
||||
~CapturedStderr()
|
||||
{
|
||||
std::cerr.rdbuf(sbuf);
|
||||
}
|
||||
|
||||
operator std::string() const { return buffer.str(); }
|
||||
};
|
||||
|
||||
#if _WIN32
|
||||
const std::string ZERO_FOUR_NAME_CONFLICT_MSG =
|
||||
"Path collision: 'C:\\data\\zero_four_2021-10.zim' and"
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
#include "../src/tools/stringTools.h"
|
||||
|
||||
#include "testing_tools.h"
|
||||
using namespace kiwix::testing;
|
||||
|
||||
const std::string ROOT_PREFIX("/ROOT%23%3F");
|
||||
|
||||
@@ -2306,3 +2308,74 @@ TEST_F(ServerTest, EmptyPatternSearchDoesNotError)
|
||||
{
|
||||
EXPECT_EQ(200, zfs1_->GET("/ROOT%23%3F/search?content=zimfile")->status);
|
||||
}
|
||||
|
||||
#define EXPECT_ERROR(MSG, SERVER_SETUP_CODE) \
|
||||
{ \
|
||||
kiwix::Server server(kiwix::Library::create()); \
|
||||
CapturedStderr stderror; \
|
||||
SERVER_SETUP_CODE; \
|
||||
EXPECT_FALSE(server.start()); \
|
||||
EXPECT_EQ(std::string(stderror), std::string("ERROR: ") + MSG + "\n"); \
|
||||
}
|
||||
|
||||
TEST(ServerNegativeTest, IpAddressAndIpModeAreMutuallyExclusive)
|
||||
{
|
||||
EXPECT_ERROR("When an IP address is provided the IP mode must not be set",
|
||||
server.setAddress("127.0.0.1");
|
||||
server.setIpMode(kiwix::IpMode::IPV4);
|
||||
);
|
||||
|
||||
EXPECT_ERROR("When an IP address is provided the IP mode must not be set",
|
||||
server.setAddress("[::1]");
|
||||
server.setIpMode(kiwix::IpMode::IPV6);
|
||||
);
|
||||
|
||||
EXPECT_ERROR("When an IP address is provided the IP mode must not be set",
|
||||
server.setAddress("localhost");
|
||||
server.setIpMode(kiwix::IpMode::ALL);
|
||||
);
|
||||
}
|
||||
|
||||
TEST(ServerNegativeTest, InvalidIpAddressDetection)
|
||||
{
|
||||
EXPECT_ERROR("invalid IP address: 1.2.3",
|
||||
server.setAddress("1.2.3");
|
||||
);
|
||||
|
||||
EXPECT_ERROR("invalid IP address: 127.0.0.256",
|
||||
server.setAddress("127.0.0.256");
|
||||
);
|
||||
|
||||
EXPECT_ERROR("invalid IP address: localhost",
|
||||
server.setAddress("localhost");
|
||||
);
|
||||
|
||||
EXPECT_ERROR("invalid IP address: fe80::94d2:16e7:5f3e:89bx",
|
||||
server.setAddress("[fe80::94d2:16e7:5f3e:89bx]");
|
||||
);
|
||||
|
||||
// We assume that our unit tests won't be run on Google's DNS server
|
||||
EXPECT_ERROR("IP address is not available on this system: 8.8.8.8",
|
||||
server.setAddress("8.8.8.8");
|
||||
);
|
||||
|
||||
// According to the spec, IPv6 addresses 2001:db8::/32 are reserved
|
||||
// for documentation and example source code
|
||||
EXPECT_ERROR("IP address is not available on this system: 2001:db8::",
|
||||
server.setAddress("[2001:db8::]");
|
||||
);
|
||||
}
|
||||
|
||||
TEST(ServerNegativeTest, UnusablePort)
|
||||
{
|
||||
// Occupy port 8910
|
||||
httplib::Server portOccupant;
|
||||
ASSERT_TRUE(portOccupant.bind_to_port("127.0.0.1", 8910));
|
||||
|
||||
// Try to listen on the same port
|
||||
EXPECT_ERROR("Unable to instantiate the HTTP daemon. The port 8910 is maybe "
|
||||
"already occupied or need more permissions to be open. Please "
|
||||
"try as root or with a port number higher or equal to 1024.",
|
||||
server.setPort(8910);
|
||||
);
|
||||
}
|
||||
|
||||
57
test/testing_tools.h
Normal file
57
test/testing_tools.h
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2026 Veloman Yunkan
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef LIBKIWIX_TESTING_TOOLS_H
|
||||
#define LIBKIWIX_TESTING_TOOLS_H
|
||||
|
||||
#include <sstream>
|
||||
#include <iostream>
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
namespace testing
|
||||
{
|
||||
|
||||
class CapturedStderr
|
||||
{
|
||||
std::ostringstream buffer;
|
||||
std::streambuf* const sbuf;
|
||||
public:
|
||||
CapturedStderr()
|
||||
: sbuf(std::cerr.rdbuf())
|
||||
{
|
||||
std::cerr.rdbuf(buffer.rdbuf());
|
||||
}
|
||||
|
||||
CapturedStderr(const CapturedStderr&) = delete;
|
||||
|
||||
~CapturedStderr()
|
||||
{
|
||||
std::cerr.rdbuf(sbuf);
|
||||
}
|
||||
|
||||
operator std::string() const { return buffer.str(); }
|
||||
};
|
||||
|
||||
} // namespace testing
|
||||
|
||||
} // namespace kiwix
|
||||
|
||||
#endif // LIBKIWIX_TESTING_TOOLS_H
|
||||
Reference in New Issue
Block a user