Line data Source code
1 : /*
2 : * Copyright (C) 2004-2026 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 : #include <opendht/thread_pool.h>
23 :
24 : #include <math.h>
25 : #include <iterator>
26 : #include <stdlib.h>
27 :
28 : using namespace std::placeholders;
29 :
30 : namespace jami {
31 :
32 : using namespace dht;
33 :
34 900 : Bucket::Bucket(const NodeId& id)
35 900 : : lowerLimit_(id)
36 900 : {}
37 :
38 : bool
39 1438 : Bucket::addNode(const std::shared_ptr<dhtnet::ChannelSocketInterface>& socket)
40 : {
41 1438 : return addNode(NodeInfo(socket));
42 : }
43 :
44 : bool
45 1730 : Bucket::addNode(NodeInfo&& info)
46 : {
47 1730 : auto nodeId = info.socket->deviceId();
48 1730 : if (nodes.try_emplace(nodeId, std::move(info)).second) {
49 1728 : connecting_nodes.erase(nodeId);
50 1729 : known_nodes.erase(nodeId);
51 1729 : mobile_nodes.erase(nodeId);
52 1729 : return true;
53 : }
54 1 : return false;
55 : }
56 :
57 : bool
58 1440 : Bucket::removeNode(const NodeId& nodeId)
59 : {
60 1440 : auto node = nodes.find(nodeId);
61 1439 : auto isMobile = node->second.isMobile_;
62 1439 : if (node == nodes.end())
63 3 : return false;
64 1435 : nodes.erase(nodeId);
65 1437 : if (isMobile) {
66 115 : addMobileNode(nodeId);
67 : } else {
68 1322 : addKnownNode(nodeId);
69 : }
70 :
71 1434 : return true;
72 : }
73 :
74 : std::set<NodeId>
75 4093 : Bucket::getNodeIds() const
76 : {
77 4093 : std::set<NodeId> nodesId;
78 15048 : for (auto const& key : nodes)
79 10968 : nodesId.insert(key.first);
80 4087 : return nodesId;
81 0 : }
82 :
83 : bool
84 9829 : Bucket::hasNode(const NodeId& nodeId) const
85 : {
86 9829 : return nodes.find(nodeId) != nodes.end();
87 : }
88 :
89 : bool
90 4421 : Bucket::addKnownNode(const NodeId& nodeId)
91 : {
92 4421 : if (!hasNode(nodeId)) {
93 3051 : if (known_nodes.emplace(nodeId).second) {
94 2742 : return true;
95 : }
96 : }
97 1682 : return false;
98 : }
99 :
100 : NodeId
101 95 : Bucket::getKnownNode(unsigned index) const
102 : {
103 95 : if (index > known_nodes.size()) {
104 1 : throw std::out_of_range("End of table for get known Node Id " + std::to_string(index));
105 : }
106 94 : auto it = known_nodes.begin();
107 : std::advance(it, index);
108 :
109 188 : return *it;
110 : }
111 :
112 : bool
113 132 : Bucket::addMobileNode(const NodeId& nodeId)
114 : {
115 132 : if (!hasNode(nodeId)) {
116 131 : if (mobile_nodes.emplace(nodeId).second) {
117 128 : known_nodes.erase(nodeId);
118 128 : return true;
119 : }
120 : }
121 4 : return false;
122 : }
123 :
124 : bool
125 1570 : Bucket::addConnectingNode(const NodeId& nodeId)
126 : {
127 1570 : if (!hasNode(nodeId)) {
128 1568 : if (connecting_nodes.emplace(nodeId).second) {
129 1462 : known_nodes.erase(nodeId);
130 1459 : mobile_nodes.erase(nodeId);
131 1457 : return true;
132 : }
133 : }
134 108 : return false;
135 : }
136 :
137 : std::set<NodeId>
138 1436 : Bucket::getKnownNodesRandom(unsigned numberNodes, std::mt19937_64& rd) const
139 : {
140 1436 : std::set<NodeId> nodesToReturn;
141 :
142 1437 : if (getKnownNodesSize() <= numberNodes)
143 1343 : return getKnownNodes();
144 :
145 94 : std::uniform_int_distribution<unsigned> distrib(0, getKnownNodesSize() - 1);
146 :
147 188 : while (nodesToReturn.size() < numberNodes) {
148 94 : nodesToReturn.emplace(getKnownNode(distrib(rd)));
149 : }
150 :
151 94 : return nodesToReturn;
152 1435 : }
153 :
154 : asio::steady_timer&
155 0 : Bucket::getNodeTimer(const std::shared_ptr<dhtnet::ChannelSocketInterface>& socket)
156 : {
157 0 : auto node = nodes.find(socket->deviceId());
158 0 : if (node == nodes.end()) {
159 0 : throw std::range_error("Unable to find timer " + socket->deviceId().toString());
160 : }
161 0 : return node->second.refresh_timer;
162 : }
163 :
164 : bool
165 13 : Bucket::shutdownNode(const NodeId& nodeId)
166 : {
167 13 : auto node = nodes.find(nodeId);
168 13 : if (node != nodes.end()) {
169 5 : auto& socket = node->second.socket;
170 5 : auto node = socket->deviceId();
171 10 : dht::ThreadPool::io().run([socket] { socket->shutdown(); });
172 5 : removeNode(node);
173 5 : return true;
174 : }
175 8 : return false;
176 : }
177 :
178 : void
179 925 : Bucket::shutdownAllNodes()
180 : {
181 1857 : while (not nodes.empty()) {
182 932 : auto it = nodes.begin();
183 1473 : dht::ThreadPool::io().run([socket = it->second.socket] { socket->shutdown(); });
184 932 : auto nodeId = it->first;
185 932 : removeNode(nodeId);
186 : }
187 925 : }
188 :
189 : void
190 0 : Bucket::printBucket(unsigned number) const
191 : {
192 0 : JAMI_ERROR("BUCKET Number: {:d}", number);
193 :
194 0 : unsigned nodeNum = 1;
195 0 : for (auto it = nodes.begin(); it != nodes.end(); ++it) {
196 0 : JAMI_DEBUG("Node {:s} Id: {:s} isMobile: {:s}",
197 : std::to_string(nodeNum),
198 : it->first.toString(),
199 : std::to_string(it->second.isMobile_));
200 0 : nodeNum++;
201 : }
202 0 : JAMI_ERROR("Mobile Nodes");
203 0 : nodeNum = 0;
204 0 : for (auto it = mobile_nodes.begin(); it != mobile_nodes.end(); ++it) {
205 0 : JAMI_DEBUG("Node {:s} Id: {:s}", std::to_string(nodeNum), (*it).toString());
206 0 : nodeNum++;
207 : }
208 :
209 0 : JAMI_ERROR("Known Nodes");
210 0 : nodeNum = 0;
211 0 : for (auto it = known_nodes.begin(); it != known_nodes.end(); ++it) {
212 0 : JAMI_DEBUG("Node {:s} Id: {:s}", std::to_string(nodeNum), (*it).toString());
213 0 : nodeNum++;
214 : }
215 0 : JAMI_ERROR("Connecting_nodes");
216 0 : nodeNum = 0;
217 0 : for (auto it = connecting_nodes.begin(); it != connecting_nodes.end(); ++it) {
218 0 : JAMI_DEBUG("Node {:s} Id: {:s}", std::to_string(nodeNum), (*it).toString());
219 0 : nodeNum++;
220 : }
221 0 : };
222 :
223 : void
224 227 : Bucket::changeMobility(const NodeId& nodeId, bool isMobile)
225 : {
226 227 : auto itn = nodes.find(nodeId);
227 228 : if (itn != nodes.end()) {
228 228 : itn->second.isMobile_ = isMobile;
229 : }
230 227 : }
231 :
232 : // For tests
233 :
234 : std::set<std::shared_ptr<dhtnet::ChannelSocketInterface>>
235 1 : Bucket::getNodeSockets() const
236 : {
237 1 : std::set<std::shared_ptr<dhtnet::ChannelSocketInterface>> sockets;
238 3 : for (auto const& info : nodes)
239 2 : sockets.insert(info.second.socket);
240 1 : return sockets;
241 0 : }
242 :
243 : // ####################################################################################################
244 :
245 588 : RoutingTable::RoutingTable()
246 : {
247 588 : buckets.emplace_back(NodeId::zero());
248 588 : }
249 :
250 : bool
251 2690 : RoutingTable::isEmpty() const
252 : {
253 3904 : for (const auto& bucket : buckets) {
254 2809 : if (!bucket.isEmpty()) {
255 1595 : return false;
256 : }
257 : }
258 1095 : return true;
259 : }
260 :
261 : bool
262 10 : RoutingTable::addNode(const std::shared_ptr<dhtnet::ChannelSocketInterface>& socket)
263 : {
264 10 : auto bucket = findBucket(socket->deviceId());
265 20 : return addNode(socket, bucket);
266 : }
267 :
268 : bool
269 2389 : RoutingTable::addNode(const std::shared_ptr<dhtnet::ChannelSocketInterface>& channel,
270 : std::list<Bucket>::iterator& bucket)
271 : {
272 2389 : NodeId nodeId = channel->deviceId();
273 :
274 2389 : if (bucket->hasNode(nodeId) || id_ == nodeId) {
275 983 : return false;
276 : }
277 :
278 1715 : while (bucket->isFull()) {
279 457 : if (contains(bucket, id_)) {
280 310 : split(bucket);
281 310 : bucket = findBucket(nodeId);
282 :
283 : } else {
284 147 : return bucket->addNode(std::move(channel));
285 : }
286 : }
287 1258 : return bucket->addNode(std::move(channel));
288 : }
289 :
290 : bool
291 500 : RoutingTable::removeNode(const NodeId& nodeId)
292 : {
293 500 : return findBucket(nodeId)->removeNode(nodeId);
294 : }
295 :
296 : bool
297 1314 : RoutingTable::hasNode(const NodeId& nodeId)
298 : {
299 1314 : return findBucket(nodeId)->hasNode(nodeId);
300 : }
301 :
302 : bool
303 3420 : RoutingTable::addKnownNode(const NodeId& nodeId)
304 : {
305 3420 : if (id_ == nodeId)
306 498 : return false;
307 :
308 2927 : auto bucket = findBucket(nodeId);
309 2927 : if (bucket == buckets.end())
310 0 : return false;
311 :
312 2928 : return bucket->addKnownNode(nodeId);
313 : }
314 :
315 : bool
316 18 : RoutingTable::addMobileNode(const NodeId& nodeId)
317 : {
318 18 : if (id_ == nodeId)
319 1 : return false;
320 :
321 17 : auto bucket = findBucket(nodeId);
322 :
323 17 : if (bucket == buckets.end())
324 0 : return false;
325 :
326 17 : bucket->addMobileNode(nodeId);
327 17 : return true;
328 : }
329 :
330 : void
331 2 : RoutingTable::removeMobileNode(const NodeId& nodeId)
332 : {
333 2 : return findBucket(nodeId)->removeMobileNode(nodeId);
334 : }
335 :
336 : bool
337 7 : RoutingTable::hasMobileNode(const NodeId& nodeId)
338 : {
339 7 : return findBucket(nodeId)->hasMobileNode(nodeId);
340 : };
341 :
342 : bool
343 1389 : RoutingTable::addConnectingNode(const NodeId& nodeId)
344 : {
345 1389 : if (id_ == nodeId)
346 1 : return false;
347 :
348 1387 : auto bucket = findBucket(nodeId);
349 :
350 1390 : if (bucket == buckets.end())
351 0 : return 0;
352 :
353 1391 : bucket->addConnectingNode(nodeId);
354 1387 : return 1;
355 : }
356 :
357 : void
358 0 : RoutingTable::removeConnectingNode(const NodeId& nodeId)
359 : {
360 0 : findBucket(nodeId)->removeConnectingNode(nodeId);
361 0 : }
362 :
363 : std::list<Bucket>::iterator
364 15522 : RoutingTable::findBucket(const NodeId& nodeId)
365 : {
366 15522 : if (buckets.empty())
367 0 : throw std::runtime_error("No bucket");
368 :
369 15515 : auto b = buckets.begin();
370 :
371 : while (true) {
372 31854 : auto next = std::next(b);
373 31854 : if (next == buckets.end())
374 15528 : return b;
375 23841 : if (std::memcmp(nodeId.data(), next->getLowerLimit().data(), nodeId.size()) < 0)
376 7511 : return b;
377 16363 : b = next;
378 16363 : }
379 : }
380 :
381 : std::vector<NodeId>
382 1303 : RoutingTable::closestNodes(const NodeId& nodeId, unsigned count)
383 : {
384 1303 : std::vector<NodeId> closestNodes;
385 1303 : auto bucket = findBucket(nodeId);
386 3723 : auto sortedBucketInsert = [&](const std::list<Bucket>::iterator& b) {
387 3723 : auto nodes = b->getNodeIds();
388 13985 : for (auto n : nodes) {
389 10274 : if (n != nodeId) {
390 8963 : auto here = std::find_if(closestNodes.begin(), closestNodes.end(), [&nodeId, &n](NodeId& NodeId) {
391 43283 : return nodeId.xorCmp(n, NodeId) < 0;
392 : });
393 :
394 8968 : closestNodes.insert(here, n);
395 : }
396 : }
397 3699 : };
398 :
399 1308 : auto itn = bucket;
400 1917 : auto itp = (bucket == buckets.begin()) ? buckets.end() : std::prev(bucket);
401 5602 : while (itn != buckets.end() || itp != buckets.end()) {
402 2989 : if (itn != buckets.end()) {
403 2485 : sortedBucketInsert(itn);
404 2477 : itn = std::next(itn);
405 : }
406 2983 : if (itp != buckets.end()) {
407 1242 : sortedBucketInsert(itp);
408 1873 : itp = (itp == buckets.begin()) ? buckets.end() : std::prev(itp);
409 : }
410 : }
411 :
412 1312 : if (closestNodes.size() > count) {
413 855 : closestNodes.resize(count);
414 : }
415 :
416 2626 : return closestNodes;
417 0 : }
418 :
419 : void
420 0 : RoutingTable::printRoutingTable() const
421 : {
422 0 : int counter = 1;
423 0 : JAMI_DEBUG("SWARM: {:s} ", id_.toString());
424 0 : for (auto it = buckets.begin(); it != buckets.end(); ++it) {
425 0 : it->printBucket(counter);
426 0 : counter++;
427 : }
428 0 : JAMI_DEBUG("_____________________________________________________________________________");
429 0 : }
430 :
431 : void
432 2 : RoutingTable::shutdownNode(const NodeId& nodeId)
433 : {
434 2 : findBucket(nodeId)->shutdownNode(nodeId);
435 2 : }
436 :
437 : std::vector<NodeId>
438 101 : RoutingTable::getNodes() const
439 : {
440 101 : std::lock_guard lock(mutex_);
441 101 : std::vector<NodeId> ret;
442 454 : for (const auto& b : buckets) {
443 353 : const auto& nodes = b.getNodeIds();
444 353 : ret.insert(ret.end(), nodes.begin(), nodes.end());
445 353 : }
446 202 : return ret;
447 101 : }
448 :
449 : std::vector<NodeId>
450 1 : RoutingTable::getKnownNodes() const
451 : {
452 1 : std::vector<NodeId> ret;
453 2 : for (const auto& b : buckets) {
454 1 : const auto& nodes = b.getKnownNodes();
455 1 : ret.insert(ret.end(), nodes.begin(), nodes.end());
456 : }
457 1 : return ret;
458 0 : }
459 :
460 : std::vector<NodeId>
461 2 : RoutingTable::getMobileNodes() const
462 : {
463 2 : std::vector<NodeId> ret;
464 4 : for (const auto& b : buckets) {
465 2 : const auto& nodes = b.getMobileNodes();
466 2 : ret.insert(ret.end(), nodes.begin(), nodes.end());
467 : }
468 2 : return ret;
469 0 : }
470 :
471 : std::vector<NodeId>
472 1 : RoutingTable::getConnectingNodes() const
473 : {
474 1 : std::vector<NodeId> ret;
475 2 : for (const auto& b : buckets) {
476 1 : const auto& nodes = b.getConnectingNodes();
477 1 : ret.insert(ret.end(), nodes.begin(), nodes.end());
478 : }
479 1 : return ret;
480 0 : }
481 :
482 : std::vector<NodeId>
483 0 : RoutingTable::getBucketMobileNodes() const
484 : {
485 0 : std::vector<NodeId> ret;
486 0 : auto bucket = findBucket(id_);
487 0 : const auto& nodes = bucket->getMobileNodes();
488 0 : ret.insert(ret.end(), nodes.begin(), nodes.end());
489 :
490 0 : return ret;
491 0 : }
492 :
493 : bool
494 4490 : RoutingTable::contains(const std::list<Bucket>::iterator& bucket, const NodeId& nodeId) const
495 : {
496 4490 : return NodeId::cmp(bucket->getLowerLimit(), nodeId) <= 0
497 10817 : && (std::next(bucket) == buckets.end() || NodeId::cmp(nodeId, std::next(bucket)->getLowerLimit()) < 0);
498 : }
499 :
500 : std::vector<NodeId>
501 16 : RoutingTable::getAllNodes() const
502 : {
503 16 : std::vector<NodeId> ret;
504 32 : for (const auto& b : buckets) {
505 16 : const auto& nodes = b.getNodeIds();
506 16 : const auto& knownNodes = b.getKnownNodes();
507 16 : const auto& mobileNodes = b.getMobileNodes();
508 16 : const auto& connectingNodes = b.getConnectingNodes();
509 16 : ret.reserve(ret.size() + nodes.size() + knownNodes.size() + mobileNodes.size() + connectingNodes.size());
510 16 : ret.insert(ret.end(), nodes.begin(), nodes.end());
511 16 : ret.insert(ret.end(), knownNodes.begin(), knownNodes.end());
512 16 : ret.insert(ret.end(), mobileNodes.begin(), mobileNodes.end());
513 16 : ret.insert(ret.end(), connectingNodes.begin(), connectingNodes.end());
514 16 : }
515 16 : return ret;
516 0 : }
517 :
518 : std::vector<NodeId>
519 1769 : RoutingTable::getConnectedNodes() const
520 : {
521 1769 : std::vector<NodeId> ret;
522 6476 : for (const auto& b : buckets) {
523 4709 : const auto& nodes = b.getNodes();
524 4709 : const auto& mobiles = b.getMobileNodes();
525 4709 : ret.reserve(ret.size() + nodes.size() + mobiles.size());
526 19689 : for (const auto& n : nodes)
527 14981 : ret.emplace_back(n.first);
528 4709 : ret.insert(ret.end(), mobiles.begin(), mobiles.end());
529 : }
530 1768 : return ret;
531 0 : }
532 :
533 : void
534 10 : RoutingTable::deleteNode(const NodeId& nodeId)
535 : {
536 10 : auto bucket = findBucket(nodeId);
537 10 : bucket->shutdownNode(nodeId);
538 10 : bucket->removeConnectingNode(nodeId);
539 10 : bucket->removeKnownNode(nodeId);
540 10 : bucket->removeMobileNode(nodeId);
541 10 : }
542 :
543 : inline std::chrono::system_clock::time_point
544 0 : systemTimeFromSteady(std::chrono::steady_clock::time_point t,
545 : const std::chrono::steady_clock::time_point& now,
546 : const std::chrono::system_clock::time_point& nowSystem)
547 : {
548 0 : return nowSystem + std::chrono::duration_cast<std::chrono::system_clock::duration>(t - now);
549 : }
550 :
551 : std::vector<RoutingTable::NodeStats>
552 0 : RoutingTable::getRoutingTableStats() const
553 : {
554 0 : std::vector<NodeStats> stats;
555 0 : auto now = std::chrono::steady_clock::now();
556 0 : auto nowSystem = std::chrono::system_clock::now();
557 0 : std::lock_guard lock(mutex_);
558 0 : for (const auto& bucket : buckets) {
559 0 : for (const auto& [id, info] : bucket.getNodes()) {
560 0 : if (auto channel = std::dynamic_pointer_cast<dhtnet::ChannelSocket>(info.socket)) {
561 0 : stats.push_back({id.toString(),
562 : "connected",
563 0 : channel->getRemoteAddress().toString(true),
564 0 : systemTimeFromSteady(channel->getStartTime(), now, nowSystem),
565 0 : info.isMobile_});
566 : } else {
567 0 : stats.push_back(
568 0 : {id.toString(), "connected", "", std::chrono::system_clock::time_point::min(), info.isMobile_});
569 0 : }
570 : }
571 0 : for (const auto& id : bucket.getKnownNodes()) {
572 0 : stats.push_back({id.toString(), "known", "", std::chrono::system_clock::time_point::min(), false});
573 : }
574 0 : for (const auto& id : bucket.getMobileNodes()) {
575 0 : stats.push_back({id.toString(), "mobile", "", std::chrono::system_clock::time_point::min(), true});
576 : }
577 0 : for (const auto& id : bucket.getConnectingNodes()) {
578 0 : stats.push_back({id.toString(), "connecting", "", std::chrono::system_clock::time_point::min(), false});
579 : }
580 : }
581 0 : return stats;
582 0 : }
583 :
584 : NodeId
585 310 : RoutingTable::middle(std::list<Bucket>::iterator& it) const
586 : {
587 310 : unsigned bit = depth(it);
588 310 : if (bit >= 8 * HASH_LEN)
589 0 : throw std::out_of_range("End of table");
590 :
591 310 : NodeId id = it->getLowerLimit();
592 310 : id.setBit(bit, true);
593 310 : return id;
594 : }
595 :
596 : unsigned
597 310 : RoutingTable::depth(std::list<Bucket>::iterator& bucket) const
598 : {
599 310 : int bit1 = bucket->getLowerLimit().lowbit();
600 760 : int bit2 = std::next(bucket) != buckets.end() ? std::next(bucket)->getLowerLimit().lowbit() : -1;
601 310 : return std::max(bit1, bit2) + 1;
602 : }
603 :
604 : bool
605 310 : RoutingTable::split(std::list<Bucket>::iterator& bucket)
606 : {
607 310 : NodeId id = middle(bucket);
608 620 : auto newBucketIt = buckets.emplace(std::next(bucket), id);
609 : // Re-assign nodes
610 310 : auto& nodeSwap = bucket->getNodes();
611 :
612 930 : for (auto it = nodeSwap.begin(); it != nodeSwap.end();) {
613 620 : auto& node = *it;
614 :
615 620 : auto nodeId = it->first;
616 :
617 620 : if (!contains(bucket, nodeId)) {
618 291 : newBucketIt->addNode(std::move(node.second));
619 291 : it = nodeSwap.erase(it);
620 : } else {
621 329 : ++it;
622 : }
623 : }
624 :
625 310 : auto connectingSwap = bucket->getConnectingNodes();
626 654 : for (auto it = connectingSwap.begin(); it != connectingSwap.end();) {
627 344 : auto nodeId = *it;
628 :
629 344 : if (!contains(bucket, nodeId)) {
630 177 : newBucketIt->addConnectingNode(nodeId);
631 177 : it = connectingSwap.erase(it);
632 177 : bucket->removeConnectingNode(nodeId);
633 : } else {
634 167 : ++it;
635 : }
636 : }
637 :
638 310 : auto knownSwap = bucket->getKnownNodes();
639 490 : for (auto it = knownSwap.begin(); it != knownSwap.end();) {
640 180 : auto nodeId = *it;
641 :
642 180 : if (!contains(bucket, nodeId)) {
643 88 : newBucketIt->addKnownNode(nodeId);
644 88 : it = knownSwap.erase(it);
645 88 : bucket->removeKnownNode(nodeId);
646 : } else {
647 92 : ++it;
648 : }
649 : }
650 :
651 310 : auto mobileSwap = bucket->getMobileNodes();
652 310 : for (auto it = mobileSwap.begin(); it != mobileSwap.end();) {
653 0 : auto nodeId = *it;
654 :
655 0 : if (!contains(bucket, nodeId)) {
656 0 : newBucketIt->addMobileNode(nodeId);
657 0 : it = mobileSwap.erase(it);
658 0 : bucket->removeMobileNode(nodeId);
659 : } else {
660 0 : ++it;
661 : }
662 : }
663 :
664 310 : return true;
665 310 : }
666 :
667 : } // namespace jami
|