From 5d7fc97148125584a44a39a4beee1e71d9518385 Mon Sep 17 00:00:00 2001 From: Edwin Mons Date: Tue, 13 Nov 2018 11:54:58 +0100 Subject: Address LinkLocal issues Generation of TXT records might fail if any of the fields is too long, so the result is now an optional (pending Expected). Callsites have been updated to deal with this. Three potentially uncaught exceptions in the Bonjour implementation have been addressed. Test-Information: Unit tests pass on macOS 10.14 and Debian 9 Change-Id: Iec02c4606a18eee855362fd3c3d15614a9e72547 diff --git a/Slimber/UnitTest/LinkLocalPresenceManagerTest.cpp b/Slimber/UnitTest/LinkLocalPresenceManagerTest.cpp index c8e7700..b1a51c9 100644 --- a/Slimber/UnitTest/LinkLocalPresenceManagerTest.cpp +++ b/Slimber/UnitTest/LinkLocalPresenceManagerTest.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -225,7 +225,9 @@ class LinkLocalPresenceManagerTest : public CppUnit::TestFixture { info.setFirstName(firstName); info.setLastName(lastName); info.setNick(nickName); - querier->setServiceInfo(service, DNSSDResolveServiceQuery::Result(name + "._presence._tcp.local", "rabbithole.local", 1234, info.toTXTRecord())); + auto txtRecord = info.toTXTRecord(); + CPPUNIT_ASSERT(txtRecord); + querier->setServiceInfo(service, DNSSDResolveServiceQuery::Result(name + "._presence._tcp.local", "rabbithole.local", 1234, *txtRecord)); querier->addService(service); eventLoop->processEvents(); } @@ -241,7 +243,9 @@ class LinkLocalPresenceManagerTest : public CppUnit::TestFixture { LinkLocalServiceInfo info; info.setStatus(status); info.setMessage(message); - querier->setServiceInfo(service, DNSSDResolveServiceQuery::Result(name + "._presence._tcp.local", "rabbithole.local", 1234, info.toTXTRecord())); + auto txtRecord = info.toTXTRecord(); + CPPUNIT_ASSERT(txtRecord); + querier->setServiceInfo(service, DNSSDResolveServiceQuery::Result(name + "._presence._tcp.local", "rabbithole.local", 1234, *txtRecord)); eventLoop->processEvents(); } diff --git a/Swiften/LinkLocal/DNSSD/Bonjour/BonjourBrowseQuery.h b/Swiften/LinkLocal/DNSSD/Bonjour/BonjourBrowseQuery.h index c049ed2..63f34db 100644 --- a/Swiften/LinkLocal/DNSSD/Bonjour/BonjourBrowseQuery.h +++ b/Swiften/LinkLocal/DNSSD/Bonjour/BonjourBrowseQuery.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -50,12 +50,17 @@ namespace Swift { } else { //std::cout << "Discovered service: name:" << name << " domain:" << domain << " type: " << type << std::endl; - DNSSDServiceID service(name, domain, type, boost::numeric_cast(interfaceIndex)); - if (flags & kDNSServiceFlagsAdd) { - eventLoop->postEvent(boost::bind(boost::ref(onServiceAdded), service), shared_from_this()); + try { + DNSSDServiceID service(name, domain, type, boost::numeric_cast(interfaceIndex)); + if (flags & kDNSServiceFlagsAdd) { + eventLoop->postEvent(boost::bind(boost::ref(onServiceAdded), service), shared_from_this()); + } + else { + eventLoop->postEvent(boost::bind(boost::ref(onServiceRemoved), service), shared_from_this()); + } } - else { - eventLoop->postEvent(boost::bind(boost::ref(onServiceRemoved), service), shared_from_this()); + catch (...) { + eventLoop->postEvent(boost::bind(boost::ref(onError)), shared_from_this()); } } } diff --git a/Swiften/LinkLocal/DNSSD/Bonjour/BonjourResolveHostnameQuery.h b/Swiften/LinkLocal/DNSSD/Bonjour/BonjourResolveHostnameQuery.h index dbf3f0e..61f000e 100644 --- a/Swiften/LinkLocal/DNSSD/Bonjour/BonjourResolveHostnameQuery.h +++ b/Swiften/LinkLocal/DNSSD/Bonjour/BonjourResolveHostnameQuery.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -23,11 +23,16 @@ namespace Swift { class BonjourResolveHostnameQuery : public DNSSDResolveHostnameQuery, public BonjourQuery { public: BonjourResolveHostnameQuery(const std::string& hostname, int interfaceIndex, std::shared_ptr querier, EventLoop* eventLoop) : BonjourQuery(querier, eventLoop) { - DNSServiceErrorType result = DNSServiceGetAddrInfo( - &sdRef, 0, boost::numeric_cast(interfaceIndex), kDNSServiceProtocol_IPv4, - hostname.c_str(), - &BonjourResolveHostnameQuery::handleHostnameResolvedStatic, this); - if (result != kDNSServiceErr_NoError) { + try { + DNSServiceErrorType result = DNSServiceGetAddrInfo( + &sdRef, 0, boost::numeric_cast(interfaceIndex), kDNSServiceProtocol_IPv4, + hostname.c_str(), + &BonjourResolveHostnameQuery::handleHostnameResolvedStatic, this); + if (result != kDNSServiceErr_NoError) { + sdRef = nullptr; + } + } + catch (...) { sdRef = nullptr; } } diff --git a/Swiften/LinkLocal/DNSSD/Bonjour/BonjourResolveServiceQuery.h b/Swiften/LinkLocal/DNSSD/Bonjour/BonjourResolveServiceQuery.h index 7a5555e..4baf87b 100644 --- a/Swiften/LinkLocal/DNSSD/Bonjour/BonjourResolveServiceQuery.h +++ b/Swiften/LinkLocal/DNSSD/Bonjour/BonjourResolveServiceQuery.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -20,12 +20,17 @@ namespace Swift { class BonjourResolveServiceQuery : public DNSSDResolveServiceQuery, public BonjourQuery { public: BonjourResolveServiceQuery(const DNSSDServiceID& service, std::shared_ptr querier, EventLoop* eventLoop) : BonjourQuery(querier, eventLoop) { - DNSServiceErrorType result = DNSServiceResolve( - &sdRef, 0, boost::numeric_cast(service.getNetworkInterfaceID()), - service.getName().c_str(), service.getType().c_str(), - service.getDomain().c_str(), - &BonjourResolveServiceQuery::handleServiceResolvedStatic, this); - if (result != kDNSServiceErr_NoError) { + try { + DNSServiceErrorType result = DNSServiceResolve( + &sdRef, 0, boost::numeric_cast(service.getNetworkInterfaceID()), + service.getName().c_str(), service.getType().c_str(), + service.getDomain().c_str(), + &BonjourResolveServiceQuery::handleServiceResolvedStatic, this); + if (result != kDNSServiceErr_NoError) { + sdRef = nullptr; + } + } + catch (...) { sdRef = nullptr; } } diff --git a/Swiften/LinkLocal/LinkLocalServiceBrowser.cpp b/Swiften/LinkLocal/LinkLocalServiceBrowser.cpp index b3328cd..0498384 100644 --- a/Swiften/LinkLocal/LinkLocalServiceBrowser.cpp +++ b/Swiften/LinkLocal/LinkLocalServiceBrowser.cpp @@ -67,15 +67,27 @@ bool LinkLocalServiceBrowser::isRegistered() const { void LinkLocalServiceBrowser::registerService(const std::string& name, unsigned short port, const LinkLocalServiceInfo& info) { assert(!registerQuery); - registerQuery = querier->createRegisterQuery(name, port, info.toTXTRecord()); - registerQuery->onRegisterFinished.connect( - boost::bind(&LinkLocalServiceBrowser::handleRegisterFinished, this, _1)); - registerQuery->registerService(); + if (auto txtRecord = info.toTXTRecord()) { + registerQuery = querier->createRegisterQuery(name, port, *txtRecord); + registerQuery->onRegisterFinished.connect( + boost::bind(&LinkLocalServiceBrowser::handleRegisterFinished, this, _1)); + registerQuery->registerService(); + } + else { + haveError = true; + stop(); + } } void LinkLocalServiceBrowser::updateService(const LinkLocalServiceInfo& info) { assert(registerQuery); - registerQuery->updateServiceInfo(info.toTXTRecord()); + if (auto txtRecord = info.toTXTRecord()) { + registerQuery->updateServiceInfo(*txtRecord); + } + else { + haveError = true; + stop(); + } } void LinkLocalServiceBrowser::unregisterService() { diff --git a/Swiften/LinkLocal/LinkLocalServiceInfo.cpp b/Swiften/LinkLocal/LinkLocalServiceInfo.cpp index 102b7f3..914fab4 100644 --- a/Swiften/LinkLocal/LinkLocalServiceInfo.cpp +++ b/Swiften/LinkLocal/LinkLocalServiceInfo.cpp @@ -11,40 +11,47 @@ #include #include +#include namespace Swift { -ByteArray LinkLocalServiceInfo::toTXTRecord() const { - ByteArray result(getEncoded("txtvers=1")); - if (!firstName.empty()) { - append(result, getEncoded("1st=" + firstName)); - } - if (!lastName.empty()) { - append(result, getEncoded("last=" + lastName)); - } - if (!email.empty()) { - append(result, getEncoded("email=" + email)); - } - if (jid.isValid()) { - append(result, getEncoded("jid=" + jid.toString())); - } - if (!message.empty()) { - append(result, getEncoded("msg=" + message)); - } - if (!nick.empty()) { - append(result, getEncoded("nick=" + nick)); - } - if (port) { - append(result, getEncoded("port.p2pj=" + std::string(std::to_string(*port)))); - } +boost::optional LinkLocalServiceInfo::toTXTRecord() const { + try { + ByteArray result(getEncoded("txtvers=1")); + if (!firstName.empty()) { + append(result, getEncoded("1st=" + firstName)); + } + if (!lastName.empty()) { + append(result, getEncoded("last=" + lastName)); + } + if (!email.empty()) { + append(result, getEncoded("email=" + email)); + } + if (jid.isValid()) { + append(result, getEncoded("jid=" + jid.toString())); + } + if (!message.empty()) { + append(result, getEncoded("msg=" + message)); + } + if (!nick.empty()) { + append(result, getEncoded("nick=" + nick)); + } + if (port) { + append(result, getEncoded("port.p2pj=" + std::string(std::to_string(*port)))); + } - switch (status) { - case Available: append(result, getEncoded("status=avail")); break; - case Away: append(result, getEncoded("status=away")); break; - case DND: append(result, getEncoded("status=dnd")); break; - } + switch (status) { + case Available: append(result, getEncoded("status=avail")); break; + case Away: append(result, getEncoded("status=away")); break; + case DND: append(result, getEncoded("status=dnd")); break; + } - return result; + return result; + } + catch (const std::exception& e) { + SWIFT_LOG(warning) << "Failed to create TXT record for link local service info: " << e.what() << std::endl; + return boost::none; + } } ByteArray LinkLocalServiceInfo::getEncoded(const std::string& s) { diff --git a/Swiften/LinkLocal/LinkLocalServiceInfo.h b/Swiften/LinkLocal/LinkLocalServiceInfo.h index eb65706..adfd062 100644 --- a/Swiften/LinkLocal/LinkLocalServiceInfo.h +++ b/Swiften/LinkLocal/LinkLocalServiceInfo.h @@ -46,7 +46,7 @@ namespace Swift { boost::optional getPort() const { return port; } void setPort(unsigned short p) { port = p; } - ByteArray toTXTRecord() const; + boost::optional toTXTRecord() const; static LinkLocalServiceInfo createFromTXTRecord(const ByteArray& record); diff --git a/Swiften/LinkLocal/UnitTest/LinkLocalConnectorTest.cpp b/Swiften/LinkLocal/UnitTest/LinkLocalConnectorTest.cpp index ab1ee0c..59cf996 100644 --- a/Swiften/LinkLocal/UnitTest/LinkLocalConnectorTest.cpp +++ b/Swiften/LinkLocal/UnitTest/LinkLocalConnectorTest.cpp @@ -115,11 +115,13 @@ class LinkLocalConnectorTest : public CppUnit::TestFixture { private: std::shared_ptr createConnector(const std::string& hostname, unsigned short port) { + auto txtRecord = LinkLocalServiceInfo().toTXTRecord(); + CPPUNIT_ASSERT(txtRecord); LinkLocalService service( DNSSDServiceID("myname", "local."), DNSSDResolveServiceQuery::Result( "myname._presence._tcp.local", hostname, port, - LinkLocalServiceInfo().toTXTRecord())); + *txtRecord)); std::shared_ptr result( new LinkLocalConnector(service, querier, connection)); result->onConnectFinished.connect( diff --git a/Swiften/LinkLocal/UnitTest/LinkLocalServiceBrowserTest.cpp b/Swiften/LinkLocal/UnitTest/LinkLocalServiceBrowserTest.cpp index a80d748..3491634 100644 --- a/Swiften/LinkLocal/UnitTest/LinkLocalServiceBrowserTest.cpp +++ b/Swiften/LinkLocal/UnitTest/LinkLocalServiceBrowserTest.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -47,10 +47,12 @@ class LinkLocalServiceBrowserTest : public CppUnit::TestFixture { eventLoop = new DummyEventLoop(); querier = std::make_shared("wonderland.lit", eventLoop); aliceServiceID = new DNSSDServiceID("alice", "wonderland.lit"); - aliceServiceInfo = new DNSSDResolveServiceQuery::Result("_presence._tcp.wonderland.lit", "xmpp.wonderland.lit", 1234, LinkLocalServiceInfo().toTXTRecord()); + auto txtRecord = LinkLocalServiceInfo().toTXTRecord(); + CPPUNIT_ASSERT(txtRecord); + aliceServiceInfo = new DNSSDResolveServiceQuery::Result("_presence._tcp.wonderland.lit", "xmpp.wonderland.lit", 1234, *txtRecord); testServiceID = new DNSSDServiceID("foo", "bar.local"); - testServiceInfo = new DNSSDResolveServiceQuery::Result("_presence._tcp.bar.local", "xmpp.bar.local", 1234, LinkLocalServiceInfo().toTXTRecord()); - testServiceInfo2 = new DNSSDResolveServiceQuery::Result("_presence.tcp.bar.local", "xmpp.foo.local", 2345, LinkLocalServiceInfo().toTXTRecord()); + testServiceInfo = new DNSSDResolveServiceQuery::Result("_presence._tcp.bar.local", "xmpp.bar.local", 1234, *txtRecord); + testServiceInfo2 = new DNSSDResolveServiceQuery::Result("_presence.tcp.bar.local", "xmpp.foo.local", 2345, *txtRecord); errorStopReceived = false; normalStopReceived = false; } @@ -292,7 +294,9 @@ class LinkLocalServiceBrowserTest : public CppUnit::TestFixture { testling->registerService("foo@bar", 1234, info); eventLoop->processEvents(); - CPPUNIT_ASSERT(querier->isServiceRegistered("foo@bar", 1234, info.toTXTRecord())); + auto txtRecord = info.toTXTRecord(); + CPPUNIT_ASSERT(txtRecord); + CPPUNIT_ASSERT(querier->isServiceRegistered("foo@bar", 1234, *txtRecord)); CPPUNIT_ASSERT_EQUAL(1, static_cast(registeredServices.size())); CPPUNIT_ASSERT(registeredServices[0] == DNSSDServiceID("foo@bar", "wonderland.lit")); testling->stop(); @@ -311,7 +315,9 @@ class LinkLocalServiceBrowserTest : public CppUnit::TestFixture { CPPUNIT_ASSERT(!testling->isRunning()); CPPUNIT_ASSERT(testling->hasError()); CPPUNIT_ASSERT(errorStopReceived); - CPPUNIT_ASSERT(!querier->isServiceRegistered("foo@bar", 1234, info.toTXTRecord())); + auto txtRecord = info.toTXTRecord(); + CPPUNIT_ASSERT(txtRecord); + CPPUNIT_ASSERT(!querier->isServiceRegistered("foo@bar", 1234, *txtRecord)); } void testRegisterService_Reregister() { @@ -329,7 +335,9 @@ class LinkLocalServiceBrowserTest : public CppUnit::TestFixture { testling->registerService("bar@baz", 3456, info); eventLoop->processEvents(); - CPPUNIT_ASSERT(querier->isServiceRegistered("bar@baz", 3456, info.toTXTRecord())); + auto txtRecord = info.toTXTRecord(); + CPPUNIT_ASSERT(txtRecord); + CPPUNIT_ASSERT(querier->isServiceRegistered("bar@baz", 3456, *txtRecord)); testling->stop(); } @@ -346,7 +354,9 @@ class LinkLocalServiceBrowserTest : public CppUnit::TestFixture { info.setFirstName("Bar"); testling->updateService(info); - CPPUNIT_ASSERT(querier->isServiceRegistered("foo@bar", 1234, info.toTXTRecord())); + auto txtRecord = info.toTXTRecord(); + CPPUNIT_ASSERT(txtRecord); + CPPUNIT_ASSERT(querier->isServiceRegistered("foo@bar", 1234, *txtRecord)); testling->stop(); } diff --git a/Swiften/LinkLocal/UnitTest/LinkLocalServiceInfoTest.cpp b/Swiften/LinkLocal/UnitTest/LinkLocalServiceInfoTest.cpp index 0a94a98..35cb1b4 100644 --- a/Swiften/LinkLocal/UnitTest/LinkLocalServiceInfoTest.cpp +++ b/Swiften/LinkLocal/UnitTest/LinkLocalServiceInfoTest.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -29,7 +29,9 @@ class LinkLocalServiceInfoTest : public CppUnit::TestFixture { info.setLastName("Tron\xc3\xe7on"); info.setStatus(LinkLocalServiceInfo::Away); - CPPUNIT_ASSERT_EQUAL(createByteArray("\x09txtvers=1\x09" + std::string("1st=Remko\x0dlast=Tron\xc3\xe7on\x0bstatus=away")), info.toTXTRecord()); + auto txtRecord = info.toTXTRecord(); + CPPUNIT_ASSERT(txtRecord); + CPPUNIT_ASSERT_EQUAL(createByteArray("\x09txtvers=1\x09" + std::string("1st=Remko\x0dlast=Tron\xc3\xe7on\x0bstatus=away")), *txtRecord); } void testCreateFromTXTRecord() { @@ -57,7 +59,9 @@ class LinkLocalServiceInfoTest : public CppUnit::TestFixture { info.setStatus(LinkLocalServiceInfo::DND); info.setPort(1234); - LinkLocalServiceInfo info2 = LinkLocalServiceInfo::createFromTXTRecord(info.toTXTRecord()); + auto txtRecord = info.toTXTRecord(); + CPPUNIT_ASSERT(txtRecord); + LinkLocalServiceInfo info2 = LinkLocalServiceInfo::createFromTXTRecord(*txtRecord); CPPUNIT_ASSERT_EQUAL(info.getFirstName(), info2.getFirstName()); CPPUNIT_ASSERT_EQUAL(info.getLastName(), info2.getLastName()); CPPUNIT_ASSERT_EQUAL(info.getEMail(), info2.getEMail()); @@ -67,6 +71,13 @@ class LinkLocalServiceInfoTest : public CppUnit::TestFixture { CPPUNIT_ASSERT(info.getStatus() == info2.getStatus()); CPPUNIT_ASSERT(info.getPort() == info2.getPort()); } + + void testToTXTRecordWithInvalidParameter() { + LinkLocalServiceInfo info; + info.setFirstName(std::string(256, 'x')); + auto txtRecord = info.toTXTRecord(); + CPPUNIT_ASSERT(!txtRecord); + } }; CPPUNIT_TEST_SUITE_REGISTRATION(LinkLocalServiceInfoTest); diff --git a/Swiften/LinkLocal/UnitTest/LinkLocalServiceTest.cpp b/Swiften/LinkLocal/UnitTest/LinkLocalServiceTest.cpp index 206d824..cb5f40a 100644 --- a/Swiften/LinkLocal/UnitTest/LinkLocalServiceTest.cpp +++ b/Swiften/LinkLocal/UnitTest/LinkLocalServiceTest.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -58,10 +58,12 @@ class LinkLocalServiceTest : public CppUnit::TestFixture { info.setFirstName(firstName); info.setLastName(lastName); info.setNick(nickName); + auto txtRecord = info.toTXTRecord(); + CPPUNIT_ASSERT(txtRecord); return LinkLocalService(service, DNSSDResolveServiceQuery::Result( name + "._presence._tcp.local", "rabbithole.local", 1234, - info.toTXTRecord())); + *txtRecord)); } }; diff --git a/Swiften/QA/DNSSDTest/DNSSDTest.cpp b/Swiften/QA/DNSSDTest/DNSSDTest.cpp index ae2fafd..5a78d2f 100644 --- a/Swiften/QA/DNSSDTest/DNSSDTest.cpp +++ b/Swiften/QA/DNSSDTest/DNSSDTest.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -35,7 +35,7 @@ class DNSSDTest : public CppUnit::TestFixture { public: void setUp() { eventLoop = new DummyEventLoop(); - querier = std::make_shared(); + querier = std::make_shared(eventLoop); querier->start(); } @@ -55,7 +55,7 @@ class DNSSDTest : public CppUnit::TestFixture { // Publish the service LinkLocalServiceInfo info; - std::shared_ptr registerQuery = querier->createRegisterQuery("DNSSDTest", 1234, info.toTXTRecord()); + std::shared_ptr registerQuery = querier->createRegisterQuery("DNSSDTest", 1234, *info.toTXTRecord()); registerQuery->onRegisterFinished.connect(boost::bind(&DNSSDTest::handleRegisterFinished, this, _1)); registerQuery->registerService(); @@ -137,7 +137,7 @@ class DNSSDTest : public CppUnit::TestFixture { private: DummyEventLoop* eventLoop; - std::shared_ptr querier; + std::shared_ptr querier; std::vector added; std::vector registered; std::vector toRemove; diff --git a/Swiften/QA/DNSSDTest/SConscript b/Swiften/QA/DNSSDTest/SConscript index 275a314..d9c9b04 100644 --- a/Swiften/QA/DNSSDTest/SConscript +++ b/Swiften/QA/DNSSDTest/SConscript @@ -7,6 +7,7 @@ if env["TEST"] : myenv.MergeFlags(myenv["CHECKER_FLAGS"]) myenv.MergeFlags(myenv["SWIFTEN_FLAGS"]) myenv.MergeFlags(myenv["CPPUNIT_FLAGS"]) + myenv.MergeFlags(myenv["GOOGLETEST_FLAGS"]) myenv.MergeFlags(myenv["BOOST_FLAGS"]) myenv.MergeFlags(myenv["LIBIDN_FLAGS"]) if myenv.get("HAVE_BONJOUR", 0) : @@ -14,6 +15,7 @@ if env["TEST"] : elif myenv.get("HAVE_AVAHI", 0) : myenv.Append(CPPDEFINES = ["HAVE_AVAHI"]) myenv.MergeFlags(myenv["AVAHI_FLAGS"]) + myenv.MergeFlags(myenv["PLATFORM_FLAGS"]) tester = myenv.Program("DNSSDTest", [ "DNSSDTest.cpp", -- cgit v0.10.2-6-g49f6