LCOV - code coverage report
Current view: top level - foo/src/jamidht/swarm - routing_table.cpp (source / functions) Hit Total Coverage
Test: jami-coverage-filtered.info Lines: 265 328 80.8 %
Date: 2025-10-16 08:11:43 Functions: 40 65 61.5 %

          Line data    Source code
       1             : /*
       2             :  *  Copyright (C) 2004-2025 Savoir-faire Linux Inc.
       3             :  *
       4             :  *  This program is free software: you can redistribute it and/or modify
       5             :  *  it under the terms of the GNU General Public License as published by
       6             :  *  the Free Software Foundation, either version 3 of the License, or
       7             :  *  (at your option) any later version.
       8             :  *
       9             :  *  This program is distributed in the hope that it will be useful,
      10             :  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
      11             :  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
      12             :  *  GNU General Public License for more details.
      13             :  *
      14             :  *  You should have received a copy of the GNU General Public License
      15             :  *  along with this program. If not, see <https://www.gnu.org/licenses/>.
      16             :  */
      17             : 
      18             : #include "routing_table.h"
      19             : 
      20             : #include <dhtnet/multiplexed_socket.h>
      21             : #include <opendht/infohash.h>
      22             : 
      23             : #include <math.h>
      24             : #include <stdio.h>
      25             : #include <iostream>
      26             : #include <iterator>
      27             : #include <stdlib.h>
      28             : #include <time.h>
      29             : 
      30             : using namespace std::placeholders;
      31             : 
      32             : namespace jami {
      33             : 
      34             : using namespace dht;
      35             : 
      36         845 : Bucket::Bucket(const NodeId& id)
      37         845 :     : lowerLimit_(id)
      38         845 : {}
      39             : 
      40             : bool
      41        1159 : Bucket::addNode(const std::shared_ptr<dhtnet::ChannelSocketInterface>& socket)
      42             : {
      43        1159 :     return addNode(NodeInfo(socket));
      44             : }
      45             : 
      46             : bool
      47        1393 : Bucket::addNode(NodeInfo&& info)
      48             : {
      49        1393 :     auto nodeId = info.socket->deviceId();
      50        1393 :     if (nodes.try_emplace(nodeId, std::move(info)).second) {
      51        1392 :         connecting_nodes.erase(nodeId);
      52        1392 :         known_nodes.erase(nodeId);
      53        1392 :         mobile_nodes.erase(nodeId);
      54        1392 :         return true;
      55             :     }
      56           1 :     return false;
      57             : }
      58             : 
      59             : bool
      60        1162 : Bucket::removeNode(const NodeId& nodeId)
      61             : {
      62        1162 :     auto node = nodes.find(nodeId);
      63        1162 :     auto isMobile = node->second.isMobile_;
      64        1162 :     if (node == nodes.end())
      65           3 :         return false;
      66        1159 :     nodes.erase(nodeId);
      67        1159 :     if (isMobile) {
      68          89 :         addMobileNode(nodeId);
      69             :     } else {
      70        1070 :         addKnownNode(nodeId);
      71             :     }
      72             : 
      73        1159 :     return true;
      74             : }
      75             : 
      76             : std::set<NodeId>
      77       10437 : Bucket::getNodeIds() const
      78             : {
      79       10437 :     std::set<NodeId> nodesId;
      80       28463 :     for (auto const& key : nodes)
      81       18026 :         nodesId.insert(key.first);
      82       10437 :     return nodesId;
      83           0 : }
      84             : 
      85             : bool
      86        8452 : Bucket::hasNode(const NodeId& nodeId) const
      87             : {
      88        8452 :     return nodes.find(nodeId) != nodes.end();
      89             : }
      90             : 
      91             : bool
      92        5349 : Bucket::addKnownNode(const NodeId& nodeId)
      93             : {
      94        5349 :     if (!hasNode(nodeId)) {
      95        4599 :         if (known_nodes.emplace(nodeId).second) {
      96        4321 :             return true;
      97             :         }
      98             :     }
      99        1028 :     return false;
     100             : }
     101             : 
     102             : NodeId
     103         205 : Bucket::getKnownNode(unsigned index) const
     104             : {
     105         205 :     if (index > known_nodes.size()) {
     106           1 :         throw std::out_of_range("End of table for get known Node Id " + std::to_string(index));
     107             :     }
     108         204 :     auto it = known_nodes.begin();
     109         204 :     std::advance(it, index);
     110             : 
     111         204 :     return *it;
     112             : }
     113             : 
     114             : bool
     115         103 : Bucket::addMobileNode(const NodeId& nodeId)
     116             : {
     117         103 :     if (!hasNode(nodeId)) {
     118         102 :         if (mobile_nodes.emplace(nodeId).second) {
     119         102 :             known_nodes.erase(nodeId);
     120         102 :             return true;
     121             :         }
     122             :     }
     123           1 :     return false;
     124             : }
     125             : 
     126             : bool
     127         991 : Bucket::addConnectingNode(const NodeId& nodeId)
     128             : {
     129         991 :     if (!hasNode(nodeId)) {
     130         991 :         if (connecting_nodes.emplace(nodeId).second) {
     131         939 :             known_nodes.erase(nodeId);
     132         939 :             mobile_nodes.erase(nodeId);
     133         939 :             return true;
     134             :         }
     135             :     }
     136          52 :     return false;
     137             : }
     138             : 
     139             : std::set<NodeId>
     140        1122 : Bucket::getKnownNodesRandom(unsigned numberNodes, std::mt19937_64& rd) const
     141             : {
     142        1122 :     std::set<NodeId> nodesToReturn;
     143             : 
     144        1122 :     if (getKnownNodesSize() <= numberNodes)
     145         973 :         return getKnownNodes();
     146             : 
     147         149 :     std::uniform_int_distribution<unsigned> distrib(0, getKnownNodesSize() - 1);
     148             : 
     149         353 :     while (nodesToReturn.size() < numberNodes) {
     150         204 :         nodesToReturn.emplace(getKnownNode(distrib(rd)));
     151             :     }
     152             : 
     153         149 :     return nodesToReturn;
     154        1122 : }
     155             : 
     156             : asio::steady_timer&
     157           0 : Bucket::getNodeTimer(const std::shared_ptr<dhtnet::ChannelSocketInterface>& socket)
     158             : {
     159           0 :     auto node = nodes.find(socket->deviceId());
     160           0 :     if (node == nodes.end()) {
     161           0 :         throw std::range_error("Unable to find timer " + socket->deviceId().toString());
     162             :     }
     163           0 :     return node->second.refresh_timer;
     164             : }
     165             : 
     166             : bool
     167          14 : Bucket::shutdownNode(const NodeId& nodeId)
     168             : {
     169          14 :     auto node = nodes.find(nodeId);
     170             : 
     171          14 :     if (node != nodes.end()) {
     172          12 :         auto socket = node->second.socket;
     173          12 :         auto node = socket->deviceId();
     174          12 :         socket->shutdown();
     175          12 :         removeNode(node);
     176          12 :         return true;
     177          12 :     }
     178           2 :     return false;
     179             : }
     180             : 
     181             : void
     182         877 : Bucket::shutdownAllNodes()
     183             : {
     184        1645 :     while (not nodes.empty()) {
     185         768 :         auto it = nodes.begin();
     186         768 :         it->second.socket->shutdown();
     187         768 :         auto nodeId = it->first;
     188         768 :         removeNode(nodeId);
     189             :     }
     190         877 : }
     191             : 
     192             : void
     193           0 : Bucket::printBucket(unsigned number) const
     194             : {
     195           0 :     JAMI_ERROR("BUCKET Number: {:d}", number);
     196             : 
     197           0 :     unsigned nodeNum = 1;
     198           0 :     for (auto it = nodes.begin(); it != nodes.end(); ++it) {
     199           0 :         JAMI_DEBUG("Node {:s}   Id: {:s}  isMobile: {:s}",
     200             :                    std::to_string(nodeNum),
     201             :                    it->first.toString(),
     202             :                    std::to_string(it->second.isMobile_));
     203           0 :         nodeNum++;
     204             :     }
     205           0 :     JAMI_ERROR("Mobile Nodes");
     206           0 :     nodeNum = 0;
     207           0 :     for (auto it = mobile_nodes.begin(); it != mobile_nodes.end(); ++it) {
     208           0 :         JAMI_DEBUG("Node {:s}   Id: {:s}", std::to_string(nodeNum), (*it).toString());
     209           0 :         nodeNum++;
     210             :     }
     211             : 
     212           0 :     JAMI_ERROR("Known Nodes");
     213           0 :     nodeNum = 0;
     214           0 :     for (auto it = known_nodes.begin(); it != known_nodes.end(); ++it) {
     215           0 :         JAMI_DEBUG("Node {:s}   Id: {:s}", std::to_string(nodeNum), (*it).toString());
     216           0 :         nodeNum++;
     217             :     }
     218           0 :     JAMI_ERROR("Connecting_nodes");
     219           0 :     nodeNum = 0;
     220           0 :     for (auto it = connecting_nodes.begin(); it != connecting_nodes.end(); ++it) {
     221           0 :         JAMI_DEBUG("Node {:s}   Id: {:s}", std::to_string(nodeNum), (*it).toString());
     222           0 :         nodeNum++;
     223             :     }
     224           0 : };
     225             : 
     226             : void
     227         176 : Bucket::changeMobility(const NodeId& nodeId, bool isMobile)
     228             : {
     229         176 :     auto itn = nodes.find(nodeId);
     230         176 :     if (itn != nodes.end()) {
     231         176 :         itn->second.isMobile_ = isMobile;
     232             :     }
     233         176 : }
     234             : 
     235             : // For tests
     236             : 
     237             : std::set<std::shared_ptr<dhtnet::ChannelSocketInterface>>
     238           1 : Bucket::getNodeSockets() const
     239             : {
     240           1 :     std::set<std::shared_ptr<dhtnet::ChannelSocketInterface>> sockets;
     241           3 :     for (auto const& info : nodes)
     242           2 :         sockets.insert(info.second.socket);
     243           1 :     return sockets;
     244           0 : }
     245             : 
     246             : // ####################################################################################################
     247             : 
     248         579 : RoutingTable::RoutingTable()
     249             : {
     250         579 :     buckets.emplace_back(NodeId::zero());
     251         579 : }
     252             : 
     253             : bool
     254          10 : RoutingTable::addNode(const std::shared_ptr<dhtnet::ChannelSocketInterface>& socket)
     255             : {
     256          10 :     auto bucket = findBucket(socket->deviceId());
     257          20 :     return addNode(socket, bucket);
     258             : }
     259             : 
     260             : bool
     261        1591 : RoutingTable::addNode(const std::shared_ptr<dhtnet::ChannelSocketInterface>& channel,
     262             :                       std::list<Bucket>::iterator& bucket)
     263             : {
     264        1591 :     NodeId nodeId = channel->deviceId();
     265             : 
     266        1591 :     if (bucket->hasNode(nodeId) || id_ == nodeId) {
     267         464 :         return false;
     268             :     }
     269             : 
     270        1391 :     while (bucket->isFull()) {
     271         374 :         if (contains(bucket, id_)) {
     272         264 :             split(bucket);
     273         264 :             bucket = findBucket(nodeId);
     274             : 
     275             :         } else {
     276         110 :             return bucket->addNode(std::move(channel));
     277             :         }
     278             :     }
     279        1017 :     return bucket->addNode(std::move(channel));
     280             : }
     281             : 
     282             : bool
     283         379 : RoutingTable::removeNode(const NodeId& nodeId)
     284             : {
     285         379 :     return findBucket(nodeId)->removeNode(nodeId);
     286             : }
     287             : 
     288             : bool
     289         410 : RoutingTable::hasNode(const NodeId& nodeId)
     290             : {
     291         410 :     return findBucket(nodeId)->hasNode(nodeId);
     292             : }
     293             : 
     294             : bool
     295        5198 : RoutingTable::addKnownNode(const NodeId& nodeId)
     296             : {
     297        5198 :     if (id_ == nodeId)
     298        1223 :         return false;
     299             : 
     300        3975 :     auto bucket = findBucket(nodeId);
     301        3975 :     if (bucket == buckets.end())
     302           0 :         return false;
     303             : 
     304        3975 :     return bucket->addKnownNode(nodeId);
     305             : }
     306             : 
     307             : bool
     308          15 : RoutingTable::addMobileNode(const NodeId& nodeId)
     309             : {
     310          15 :     if (id_ == nodeId)
     311           1 :         return false;
     312             : 
     313          14 :     auto bucket = findBucket(nodeId);
     314             : 
     315          14 :     if (bucket == buckets.end())
     316           0 :         return 0;
     317             : 
     318          14 :     bucket->addMobileNode(nodeId);
     319          14 :     return 1;
     320             : }
     321             : 
     322             : void
     323           2 : RoutingTable::removeMobileNode(const NodeId& nodeId)
     324             : {
     325           2 :     return findBucket(nodeId)->removeMobileNode(nodeId);
     326             : }
     327             : 
     328             : bool
     329           7 : RoutingTable::hasMobileNode(const NodeId& nodeId)
     330             : {
     331           7 :     return findBucket(nodeId)->hasMobileNode(nodeId);
     332             : };
     333             : 
     334             : bool
     335         887 : RoutingTable::addConnectingNode(const NodeId& nodeId)
     336             : {
     337         887 :     if (id_ == nodeId)
     338           1 :         return false;
     339             : 
     340         886 :     auto bucket = findBucket(nodeId);
     341             : 
     342         886 :     if (bucket == buckets.end())
     343           0 :         return 0;
     344             : 
     345         886 :     bucket->addConnectingNode(nodeId);
     346         886 :     return 1;
     347             : }
     348             : 
     349             : void
     350           0 : RoutingTable::removeConnectingNode(const NodeId& nodeId)
     351             : {
     352           0 :     findBucket(nodeId)->removeConnectingNode(nodeId);
     353           0 : }
     354             : 
     355             : std::list<Bucket>::iterator
     356       12766 : RoutingTable::findBucket(const NodeId& nodeId)
     357             : {
     358       12766 :     if (buckets.empty())
     359           0 :         throw std::runtime_error("No bucket");
     360             : 
     361       12766 :     auto b = buckets.begin();
     362             : 
     363             :     while (true) {
     364       19517 :         auto next = std::next(b);
     365       19517 :         if (next == buckets.end())
     366       12766 :             return b;
     367       10664 :         if (std::memcmp(nodeId.data(), next->getLowerLimit().data(), nodeId.size()) < 0)
     368        3913 :             return b;
     369        6751 :         b = next;
     370        6751 :     }
     371             : }
     372             : 
     373             : std::vector<NodeId>
     374        1036 : RoutingTable::closestNodes(const NodeId& nodeId, unsigned count)
     375             : {
     376        1036 :     std::vector<NodeId> closestNodes;
     377        1036 :     auto bucket = findBucket(nodeId);
     378        2392 :     auto sortedBucketInsert = [&](const std::list<Bucket>::iterator& b) {
     379        2392 :         auto nodes = b->getNodeIds();
     380        6894 :         for (auto n : nodes) {
     381        4502 :             if (n != nodeId) {
     382        3468 :                 auto here = std::find_if(closestNodes.begin(),
     383             :                                          closestNodes.end(),
     384        9516 :                                          [&nodeId, &n](NodeId& NodeId) {
     385        9516 :                                              return nodeId.xorCmp(n, NodeId) < 0;
     386             :                                          });
     387             : 
     388        3468 :                 closestNodes.insert(here, n);
     389             :             }
     390             :         }
     391        2392 :     };
     392             : 
     393        1036 :     auto itn = bucket;
     394        1036 :     auto itp = (bucket == buckets.begin()) ? buckets.end() : std::prev(bucket);
     395        2934 :     while (itn != buckets.end() || itp != buckets.end()) {
     396        1898 :         if (itn != buckets.end()) {
     397        1673 :             sortedBucketInsert(itn);
     398        1673 :             itn = std::next(itn);
     399             :         }
     400        1898 :         if (itp != buckets.end()) {
     401         719 :             sortedBucketInsert(itp);
     402         719 :             itp = (itp == buckets.begin()) ? buckets.end() : std::prev(itp);
     403             :         }
     404             :     }
     405             : 
     406        1036 :     if (closestNodes.size() > count) {
     407         491 :         closestNodes.resize(count);
     408             :     }
     409             : 
     410        2072 :     return closestNodes;
     411           0 : }
     412             : 
     413             : void
     414           0 : RoutingTable::printRoutingTable() const
     415             : {
     416           0 :     int counter = 1;
     417           0 :     JAMI_DEBUG("SWARM: {:s} ", id_.toString());
     418           0 :     for (auto it = buckets.begin(); it != buckets.end(); ++it) {
     419           0 :         it->printBucket(counter);
     420           0 :         counter++;
     421             :     }
     422           0 :     JAMI_DEBUG("_____________________________________________________________________________");
     423           0 : }
     424             : 
     425             : void
     426           2 : RoutingTable::shutdownNode(const NodeId& nodeId)
     427             : {
     428           2 :     findBucket(nodeId)->shutdownNode(nodeId);
     429           2 : }
     430             : 
     431             : std::vector<NodeId>
     432        4157 : RoutingTable::getNodes() const
     433             : {
     434        4157 :     std::lock_guard lock(mutex_);
     435        4157 :     std::vector<NodeId> ret;
     436       12179 :     for (const auto& b : buckets) {
     437        8022 :         const auto& nodes = b.getNodeIds();
     438        8022 :         ret.insert(ret.end(), nodes.begin(), nodes.end());
     439        8022 :     }
     440        8314 :     return ret;
     441        4157 : }
     442             : 
     443             : std::vector<NodeId>
     444           1 : RoutingTable::getKnownNodes() const
     445             : {
     446           1 :     std::vector<NodeId> ret;
     447           2 :     for (const auto& b : buckets) {
     448           1 :         const auto& nodes = b.getKnownNodes();
     449           1 :         ret.insert(ret.end(), nodes.begin(), nodes.end());
     450             :     }
     451           1 :     return ret;
     452           0 : }
     453             : 
     454             : std::vector<NodeId>
     455        1755 : RoutingTable::getMobileNodes() const
     456             : {
     457        1755 :     std::vector<NodeId> ret;
     458        5360 :     for (const auto& b : buckets) {
     459        3605 :         const auto& nodes = b.getMobileNodes();
     460        3605 :         ret.insert(ret.end(), nodes.begin(), nodes.end());
     461             :     }
     462        1755 :     return ret;
     463           0 : }
     464             : 
     465             : std::vector<NodeId>
     466           1 : RoutingTable::getConnectingNodes() const
     467             : {
     468           1 :     std::vector<NodeId> ret;
     469           2 :     for (const auto& b : buckets) {
     470           1 :         const auto& nodes = b.getConnectingNodes();
     471           1 :         ret.insert(ret.end(), nodes.begin(), nodes.end());
     472             :     }
     473           1 :     return ret;
     474           0 : }
     475             : 
     476             : std::vector<NodeId>
     477           0 : RoutingTable::getBucketMobileNodes() const
     478             : {
     479           0 :     std::vector<NodeId> ret;
     480           0 :     auto bucket = findBucket(id_);
     481           0 :     const auto& nodes = bucket->getMobileNodes();
     482           0 :     ret.insert(ret.end(), nodes.begin(), nodes.end());
     483             : 
     484           0 :     return ret;
     485           0 : }
     486             : 
     487             : bool
     488        3548 : RoutingTable::contains(const std::list<Bucket>::iterator& bucket, const NodeId& nodeId) const
     489             : {
     490        3548 :     return NodeId::cmp(bucket->getLowerLimit(), nodeId) <= 0
     491        5765 :            && (std::next(bucket) == buckets.end()
     492        5765 :                || NodeId::cmp(nodeId, std::next(bucket)->getLowerLimit()) < 0);
     493             : }
     494             : 
     495             : std::vector<NodeId>
     496          16 : RoutingTable::getAllNodes() const
     497             : {
     498          16 :     std::vector<NodeId> ret;
     499          32 :     for (const auto& b : buckets) {
     500          16 :         const auto& nodes = b.getNodeIds();
     501          16 :         const auto& knownNodes = b.getKnownNodes();
     502          16 :         const auto& mobileNodes = b.getMobileNodes();
     503          16 :         const auto& connectingNodes = b.getConnectingNodes();
     504          16 :         ret.reserve(nodes.size() + knownNodes.size() + mobileNodes.size() + connectingNodes.size());
     505          16 :         ret.insert(ret.end(), nodes.begin(), nodes.end());
     506          16 :         ret.insert(ret.end(), knownNodes.begin(), knownNodes.end());
     507          16 :         ret.insert(ret.end(), mobileNodes.begin(), mobileNodes.end());
     508          16 :         ret.insert(ret.end(), connectingNodes.begin(), connectingNodes.end());
     509          16 :     }
     510          16 :     return ret;
     511           0 : }
     512             : 
     513             : void
     514          11 : RoutingTable::deleteNode(const NodeId& nodeId)
     515             : {
     516          11 :     auto bucket = findBucket(nodeId);
     517          11 :     bucket->shutdownNode(nodeId);
     518          11 :     bucket->removeConnectingNode(nodeId);
     519          11 :     bucket->removeKnownNode(nodeId);
     520          11 :     bucket->removeMobileNode(nodeId);
     521          11 : }
     522             : 
     523             : NodeId
     524         264 : RoutingTable::middle(std::list<Bucket>::iterator& it) const
     525             : {
     526         264 :     unsigned bit = depth(it);
     527         264 :     if (bit >= 8 * HASH_LEN)
     528           0 :         throw std::out_of_range("End of table");
     529             : 
     530         264 :     NodeId id = it->getLowerLimit();
     531         264 :     id.setBit(bit, true);
     532         264 :     return id;
     533             : }
     534             : 
     535             : unsigned
     536         264 : RoutingTable::depth(std::list<Bucket>::iterator& bucket) const
     537             : {
     538         264 :     int bit1 = bucket->getLowerLimit().lowbit();
     539         264 :     int bit2 = std::next(bucket) != buckets.end() ? std::next(bucket)->getLowerLimit().lowbit()
     540         264 :                                                   : -1;
     541         264 :     return std::max(bit1, bit2) + 1;
     542             : }
     543             : 
     544             : bool
     545         264 : RoutingTable::split(std::list<Bucket>::iterator& bucket)
     546             : {
     547         264 :     NodeId id = middle(bucket);
     548         264 :     auto newBucketIt = buckets.emplace(std::next(bucket), id);
     549             :     // Re-assign nodes
     550         264 :     auto& nodeSwap = bucket->getNodes();
     551             : 
     552         792 :     for (auto it = nodeSwap.begin(); it != nodeSwap.end();) {
     553         528 :         auto& node = *it;
     554             : 
     555         528 :         auto nodeId = it->first;
     556             : 
     557         528 :         if (!contains(bucket, nodeId)) {
     558         233 :             newBucketIt->addNode(std::move(node.second));
     559         233 :             it = nodeSwap.erase(it);
     560             :         } else {
     561         295 :             ++it;
     562             :         }
     563             :     }
     564             : 
     565         264 :     auto connectingSwap = bucket->getConnectingNodes();
     566         476 :     for (auto it = connectingSwap.begin(); it != connectingSwap.end();) {
     567         212 :         auto nodeId = *it;
     568             : 
     569         212 :         if (!contains(bucket, nodeId)) {
     570         103 :             newBucketIt->addConnectingNode(nodeId);
     571         103 :             it = connectingSwap.erase(it);
     572         103 :             bucket->removeConnectingNode(nodeId);
     573             :         } else {
     574         109 :             ++it;
     575             :         }
     576             :     }
     577             : 
     578         264 :     auto knownSwap = bucket->getKnownNodes();
     579         714 :     for (auto it = knownSwap.begin(); it != knownSwap.end();) {
     580         450 :         auto nodeId = *it;
     581             : 
     582         450 :         if (!contains(bucket, nodeId)) {
     583         239 :             newBucketIt->addKnownNode(nodeId);
     584         239 :             it = knownSwap.erase(it);
     585         239 :             bucket->removeKnownNode(nodeId);
     586             :         } else {
     587         211 :             ++it;
     588             :         }
     589             :     }
     590             : 
     591         264 :     auto mobileSwap = bucket->getMobileNodes();
     592         264 :     for (auto it = mobileSwap.begin(); it != mobileSwap.end();) {
     593           0 :         auto nodeId = *it;
     594             : 
     595           0 :         if (!contains(bucket, nodeId)) {
     596           0 :             newBucketIt->addMobileNode(nodeId);
     597           0 :             it = mobileSwap.erase(it);
     598           0 :             bucket->removeMobileNode(nodeId);
     599             :         } else {
     600           0 :             ++it;
     601             :         }
     602             :     }
     603             : 
     604         264 :     return true;
     605         264 : }
     606             : 
     607             : } // namespace jami

Generated by: LCOV version 1.14