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 : #include "gitserver.h"
18 :
19 : #include "fileutils.h"
20 : #include "logger.h"
21 : #include "gittransport.h"
22 : #include "manager.h"
23 : #include <opendht/thread_pool.h>
24 : #include <dhtnet/multiplexed_socket.h>
25 : #include <fmt/compile.h>
26 :
27 : #include <charconv>
28 : #include <ctime>
29 : #include <fstream>
30 : #include <git2.h>
31 : #include <iomanip>
32 :
33 : using namespace std::string_view_literals;
34 : constexpr auto FLUSH_PKT = "0000"sv;
35 : constexpr auto NAK_PKT = "0008NAK\n"sv;
36 : constexpr auto DONE_CMD = "done\n"sv;
37 : constexpr auto WANT_CMD = "want"sv;
38 : constexpr auto HAVE_CMD = "have"sv;
39 : constexpr auto SERVER_CAPABILITIES = " HEAD\0side-band side-band-64k shallow no-progress include-tag"sv;
40 :
41 : namespace jami {
42 :
43 : class GitServer::Impl
44 : {
45 : public:
46 1529 : Impl(const std::string& accountId,
47 : const std::string& repositoryId,
48 : const std::string& repository,
49 : const std::shared_ptr<dhtnet::ChannelSocketInterface>& socket)
50 1529 : : accountId_(accountId)
51 1529 : , repositoryId_(repositoryId)
52 1529 : , repository_(repository)
53 1529 : , socket_(socket)
54 : {
55 : // JAMI_DEBUG("[Account {}] [Conversation {}] [GitServer {}] created",
56 : // accountId_, repositoryId_, fmt::ptr(this));
57 : // Check at least if repository is correct
58 : git_repository* repo;
59 1529 : if (git_repository_open(&repo, repository_.c_str()) != 0) {
60 92 : dht::ThreadPool::io().run([socket = socket_] { socket->shutdown(); });
61 46 : return;
62 : }
63 1483 : git_repository_free(repo);
64 :
65 1483 : socket_->setOnRecv([this](const uint8_t* buf, std::size_t len) {
66 6671 : if (parseOrder(std::string_view((const char*) buf, len)))
67 16942 : while (parseOrder())
68 : ;
69 6671 : return len;
70 : });
71 0 : }
72 1529 : ~Impl()
73 : {
74 1529 : stop();
75 : // JAMI_DEBUG("[Account {}] [Conversation {}] [GitServer {}] destroyed", accountId_, repositoryId_, fmt::ptr(this));
76 1529 : }
77 3785 : void stop()
78 : {
79 3785 : if (isDestroying_.exchange(true))
80 2256 : return;
81 : // Clearing the receive callback waits for an in-flight invocation to
82 : // return, so no order can be parsed against a half-destroyed Impl once
83 : // this call completes.
84 1529 : socket_->setOnRecv({});
85 3058 : dht::ThreadPool::io().run([socket = socket_] { socket->shutdown(); });
86 : }
87 : bool parseOrder(std::string_view buf = {});
88 :
89 : void sendReferenceCapabilities(bool sendVersion = false);
90 : bool NAK();
91 : void notifyUpToDate();
92 : void ACKCommon();
93 : bool ACKFirst();
94 : void sendPackData();
95 : std::map<std::string, std::string> getParameters(std::string_view pkt_line);
96 :
97 : std::string accountId_ {};
98 : std::string repositoryId_ {};
99 : std::string repository_ {};
100 : std::shared_ptr<dhtnet::ChannelSocketInterface> socket_ {};
101 : std::string wantedReference_ {};
102 : // HEAD announced during the reference advertisement, used to acknowledge peers
103 : // that are already up to date and therefore never send a want.
104 : std::string advertisedHead_ {};
105 : bool sawWant_ {false};
106 : bool upToDateNotified_ {false};
107 : std::string common_ {};
108 : std::vector<std::string> haveRefs_ {};
109 : std::string cachedPkt_ {};
110 : std::atomic_bool isDestroying_ {false};
111 : onFetchedCb onFetchedCb_ {};
112 : };
113 :
114 : bool
115 23613 : GitServer::Impl::parseOrder(std::string_view buf)
116 : {
117 23613 : std::string pkt = std::move(cachedPkt_);
118 23613 : if (!buf.empty())
119 6671 : pkt += buf;
120 :
121 : // Parse pkt len
122 : // Reference: https://github.com/git/git/blob/master/Documentation/technical/protocol-common.txt#L51
123 : // The first four bytes define the length of the packet and 0000 is a FLUSH pkt
124 :
125 23613 : unsigned int pkt_len = 0;
126 23613 : auto [p, ec] = std::from_chars(pkt.data(), pkt.data() + 4, pkt_len, 16);
127 23613 : if (ec != std::errc()) {
128 0 : JAMI_ERROR("[Account {}] [Conversation {}] [GitServer {}] Unable to parse packet size",
129 : accountId_,
130 : repositoryId_,
131 : fmt::ptr(this));
132 : }
133 23613 : if (pkt_len != pkt.size()) {
134 : // Store next packet part
135 19742 : if (pkt_len == 0) {
136 : // FLUSH_PKT
137 4415 : pkt_len = 4;
138 : }
139 19742 : cachedPkt_ = pkt.substr(pkt_len);
140 : }
141 :
142 23613 : auto pack = std::string_view(pkt).substr(4, pkt_len - 4);
143 23613 : if (pack == DONE_CMD) {
144 : // Reference:
145 : // https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L390 Do
146 : // not do multi-ack, just send ACK + pack file
147 : // In case of no common base, send NAK
148 1617 : JAMI_LOG("[Account {}] [Conversation {}] [GitServer {}] Peer negotiation is done. Answering to want order",
149 : accountId_,
150 : repositoryId_,
151 : fmt::ptr(this));
152 : bool sendData;
153 1617 : if (common_.empty())
154 430 : sendData = NAK();
155 : else
156 1187 : sendData = ACKFirst();
157 1617 : if (sendData)
158 1617 : sendPackData();
159 1617 : return !cachedPkt_.empty();
160 21996 : } else if (pack.empty()) {
161 4415 : if (!haveRefs_.empty()) {
162 : // Reference:
163 : // https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L390
164 : // Do not do multi-ack, just send ACK + pack file In case of no common base ACK
165 562 : ACKCommon();
166 562 : NAK();
167 : } else {
168 : // A flush right after the reference advertisement, with neither a want nor a have,
169 : // is the peer telling us it already owns everything we announced. No pack is sent,
170 : // so the regular fetch acknowledgement never fires and the messages we advertised
171 : // would stay marked as sending forever. Acknowledge the announced HEAD instead.
172 3853 : notifyUpToDate();
173 : }
174 4415 : return !cachedPkt_.empty();
175 : }
176 :
177 17581 : auto lim = pack.find(' ');
178 17581 : auto cmd = pack.substr(0, lim);
179 17581 : auto dat = (lim < pack.size()) ? pack.substr(lim + 1) : std::string_view {};
180 17581 : if (cmd == UPLOAD_PACK_CMD) {
181 : // Cf: https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L166
182 : // References discovery
183 : /*JAMI_LOG("[Account {}] [Conversation {}] [GitServer {}] Upload pack command detected.",
184 : accountId_,
185 : repositoryId_,
186 : fmt::ptr(this));*/
187 2250 : auto version = 1;
188 2250 : auto parameters = getParameters(dat);
189 2250 : auto versionIt = parameters.find("version");
190 2250 : bool sendVersion = false;
191 2250 : if (versionIt != parameters.end()) {
192 0 : auto [p, ec] = std::from_chars(versionIt->second.data(),
193 0 : versionIt->second.data() + versionIt->second.size(),
194 : version);
195 0 : if (ec == std::errc()) {
196 0 : sendVersion = true;
197 : } else {
198 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Invalid version detected: {}",
199 : accountId_,
200 : repositoryId_,
201 : fmt::ptr(this),
202 : versionIt->second);
203 : }
204 : }
205 2250 : if (version == 1) {
206 2250 : sendReferenceCapabilities(sendVersion);
207 : } else {
208 0 : JAMI_ERROR("[Account {}] [Conversation {}] [GitServer {}] That protocol version is not yet supported "
209 : "(version: {:d})",
210 : accountId_,
211 : repositoryId_,
212 : fmt::ptr(this),
213 : version);
214 : }
215 17581 : } else if (cmd == WANT_CMD) {
216 : // Reference:
217 : // https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L229
218 : // TODO can have more want
219 1617 : wantedReference_ = dat.substr(0, 40);
220 1617 : sawWant_ = true;
221 1617 : JAMI_LOG("[Account {}] [Conversation {}] [GitServer {}] Peer want ref: {}",
222 : accountId_,
223 : repositoryId_,
224 : fmt::ptr(this),
225 : wantedReference_);
226 13714 : } else if (cmd == HAVE_CMD) {
227 13714 : const auto& commit = haveRefs_.emplace_back(dat.substr(0, 40));
228 13714 : if (common_.empty()) {
229 : // Detect first common commit
230 : // Reference:
231 : // https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L390
232 : // TODO do not open repository every time
233 : git_repository* repo;
234 1187 : if (git_repository_open(&repo, repository_.c_str()) != 0) {
235 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to open {}",
236 : accountId_,
237 : repositoryId_,
238 : fmt::ptr(this),
239 : repository_);
240 0 : return !cachedPkt_.empty();
241 : }
242 1187 : GitRepository rep {repo};
243 : git_oid commit_id;
244 1187 : if (git_oid_fromstr(&commit_id, commit.c_str()) == 0) {
245 : // Reference found
246 1187 : common_ = commit;
247 : }
248 1187 : }
249 : } else {
250 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unwanted packet received: {}",
251 : accountId_,
252 : repositoryId_,
253 : fmt::ptr(this),
254 : pkt);
255 : }
256 17581 : return !cachedPkt_.empty();
257 23613 : }
258 :
259 : std::string
260 26563 : toGitHex(size_t value)
261 : {
262 106252 : return fmt::format(FMT_COMPILE("{:04x}"), value & 0x0FFFF);
263 : }
264 :
265 : void
266 2250 : GitServer::Impl::sendReferenceCapabilities(bool sendVersion)
267 : {
268 : // Get references
269 : // First, get the HEAD reference
270 : // https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L166
271 : git_repository* repo;
272 2250 : if (git_repository_open(&repo, repository_.c_str()) != 0) {
273 1 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to open {}",
274 : accountId_,
275 : repositoryId_,
276 : fmt::ptr(this),
277 : repository_);
278 2 : dht::ThreadPool::io().run([socket = socket_] { socket->shutdown(); });
279 1 : return;
280 : }
281 2249 : GitRepository rep {repo};
282 :
283 : // Answer with the version number
284 : // **** When the client initially connects the server will immediately respond
285 : // **** with a version number (if "version=1" is sent as an Extra Parameter),
286 2249 : std::error_code ec;
287 2249 : if (sendVersion) {
288 0 : constexpr auto toSend = "000eversion 1\0"sv;
289 0 : socket_->write(reinterpret_cast<const unsigned char*>(toSend.data()), toSend.size(), ec);
290 0 : if (ec) {
291 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to send data for {}: {}",
292 : accountId_,
293 : repositoryId_,
294 : fmt::ptr(this),
295 : repository_,
296 : ec.message());
297 0 : dht::ThreadPool::io().run([socket = socket_] { socket->shutdown(); });
298 0 : return;
299 : }
300 : }
301 :
302 : git_oid commit_id;
303 2249 : if (git_reference_name_to_id(&commit_id, rep.get(), "HEAD") < 0) {
304 0 : JAMI_ERROR("[Account {}] [Conversation {}] [GitServer {}] Unable to get reference for HEAD",
305 : accountId_,
306 : repositoryId_,
307 : fmt::ptr(this));
308 0 : dht::ThreadPool::io().run([socket = socket_] { socket->shutdown(); });
309 0 : return;
310 : }
311 2249 : std::string_view currentHead = git_oid_tostr_s(&commit_id);
312 2249 : advertisedHead_ = currentHead;
313 :
314 : // Send references
315 2249 : std::ostringstream packet;
316 2249 : packet << toGitHex(5 + currentHead.size() + SERVER_CAPABILITIES.size());
317 2249 : packet << currentHead << SERVER_CAPABILITIES << "\n";
318 :
319 : // Now, add other references
320 : git_strarray refs;
321 2249 : if (git_reference_list(&refs, rep.get()) == 0) {
322 24926 : for (std::size_t i = 0; i < refs.count; ++i) {
323 22677 : std::string_view ref = refs.strings[i];
324 22677 : if (git_reference_name_to_id(&commit_id, rep.get(), ref.data()) < 0) {
325 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to get reference for {}",
326 : accountId_,
327 : repositoryId_,
328 : fmt::ptr(this),
329 : ref);
330 0 : continue;
331 : }
332 22677 : currentHead = git_oid_tostr_s(&commit_id);
333 :
334 22677 : packet << toGitHex(6 /* size + space + \n */ + currentHead.size() + ref.size());
335 22677 : packet << currentHead << " " << ref << "\n";
336 : }
337 : }
338 2249 : git_strarray_dispose(&refs);
339 :
340 : // And add FLUSH
341 2249 : packet << FLUSH_PKT;
342 2249 : auto toSend = packet.str();
343 2249 : socket_->write(reinterpret_cast<const unsigned char*>(toSend.data()), toSend.size(), ec);
344 2249 : if (ec) {
345 1 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to send data for {}: {}",
346 : accountId_,
347 : repositoryId_,
348 : fmt::ptr(this),
349 : repository_,
350 : ec.message());
351 2 : dht::ThreadPool::io().run([socket = socket_] { socket->shutdown(); });
352 : }
353 2249 : }
354 :
355 : void
356 3853 : GitServer::Impl::notifyUpToDate()
357 : {
358 3853 : if (sawWant_ || upToDateNotified_ || advertisedHead_.empty())
359 3226 : return;
360 627 : upToDateNotified_ = true;
361 : /*JAMI_LOG("[Account {}] [Conversation {}] [GitServer {}] Peer is already up to date at {}",
362 : accountId_,
363 : repositoryId_,
364 : fmt::ptr(this),
365 : advertisedHead_);*/
366 627 : if (onFetchedCb_)
367 627 : onFetchedCb_(advertisedHead_);
368 : }
369 :
370 : void
371 562 : GitServer::Impl::ACKCommon()
372 : {
373 562 : std::error_code ec;
374 : // Ack common base
375 562 : if (!common_.empty()) {
376 1124 : auto toSend = fmt::format(FMT_COMPILE("{:04x}ACK {} continue\n"),
377 562 : 18 + common_.size() /* size + ACK + space * 2 + continue + \n */,
378 562 : common_);
379 562 : socket_->write(reinterpret_cast<const unsigned char*>(toSend.c_str()), toSend.size(), ec);
380 562 : if (ec) {
381 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to send data for {}: {}",
382 : accountId_,
383 : repositoryId_,
384 : fmt::ptr(this),
385 : repository_,
386 : ec.message());
387 0 : dht::ThreadPool::io().run([socket = socket_] { socket->shutdown(); });
388 : }
389 562 : }
390 562 : }
391 :
392 : bool
393 1187 : GitServer::Impl::ACKFirst()
394 : {
395 1187 : std::error_code ec;
396 : // Ack common base
397 1187 : if (!common_.empty()) {
398 2374 : auto toSend = fmt::format(FMT_COMPILE("{:04x}ACK {}\n"),
399 1187 : 9 + common_.size() /* size + ACK + space + \n */,
400 1187 : common_);
401 1187 : socket_->write(reinterpret_cast<const unsigned char*>(toSend.c_str()), toSend.size(), ec);
402 1187 : if (ec) {
403 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to send data for {}: {}",
404 : accountId_,
405 : repositoryId_,
406 : fmt::ptr(this),
407 : repository_,
408 : ec.message());
409 0 : dht::ThreadPool::io().run([socket = socket_] { socket->shutdown(); });
410 0 : return false;
411 : }
412 1187 : }
413 1187 : return true;
414 : }
415 :
416 : bool
417 992 : GitServer::Impl::NAK()
418 : {
419 992 : std::error_code ec;
420 : // NAK
421 992 : socket_->write(reinterpret_cast<const unsigned char*>(NAK_PKT.data()), NAK_PKT.size(), ec);
422 992 : if (ec) {
423 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to send data for {}: {}",
424 : accountId_,
425 : repositoryId_,
426 : fmt::ptr(this),
427 : repository_,
428 : ec.message());
429 0 : dht::ThreadPool::io().run([socket = socket_] { socket->shutdown(); });
430 0 : return false;
431 : }
432 992 : return true;
433 : }
434 :
435 : void
436 1617 : GitServer::Impl::sendPackData()
437 : {
438 : git_repository* repo_ptr;
439 1617 : if (git_repository_open(&repo_ptr, repository_.c_str()) != 0) {
440 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to open {}",
441 : accountId_,
442 : repositoryId_,
443 : fmt::ptr(this),
444 : repository_);
445 0 : return;
446 : }
447 1617 : GitRepository repo {repo_ptr};
448 :
449 : git_packbuilder* pb_ptr;
450 1617 : if (git_packbuilder_new(&pb_ptr, repo.get()) != 0) {
451 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to open packbuilder for {}",
452 : accountId_,
453 : repositoryId_,
454 : fmt::ptr(this),
455 : repository_);
456 0 : return;
457 : }
458 1617 : GitPackBuilder pb {pb_ptr};
459 :
460 1617 : std::string fetched = wantedReference_;
461 : git_oid oid;
462 1617 : if (git_oid_fromstr(&oid, fetched.c_str()) < 0) {
463 0 : JAMI_ERROR("[Account {}] [Conversation {}] [GitServer {}] Unable to get reference for commit {}",
464 : accountId_,
465 : repositoryId_,
466 : fmt::ptr(this),
467 : fetched);
468 0 : return;
469 : }
470 :
471 1617 : git_revwalk* walker_ptr = nullptr;
472 1617 : if (git_revwalk_new(&walker_ptr, repo.get()) < 0 || git_revwalk_push(walker_ptr, &oid) < 0) {
473 0 : if (walker_ptr)
474 0 : git_revwalk_free(walker_ptr);
475 0 : return;
476 : }
477 1617 : GitRevWalker walker {walker_ptr};
478 1617 : git_revwalk_sorting(walker.get(), GIT_SORT_TOPOLOGICAL);
479 :
480 : // Hide the commits the peer has. git_packbuilder_insert_walk() then skips
481 : // them along with the trees and blobs they reference, rather than sending
482 : // the whole tree, every certificate included, with each new commit.
483 1617 : git_odb* odb_ptr = nullptr;
484 1617 : if (git_repository_odb(&odb_ptr, repo.get()) == 0) {
485 1617 : GitOdb odb {odb_ptr};
486 15331 : for (const auto& have : haveRefs_) {
487 : // Peers also announce commits we lack; skip those without rescanning the packs.
488 : git_oid haveOid;
489 13714 : if (git_oid_fromstr(&haveOid, have.c_str()) == 0
490 13714 : && git_odb_exists_ext(odb.get(), &haveOid, GIT_ODB_LOOKUP_NO_REFRESH))
491 13694 : git_revwalk_hide(walker.get(), &haveOid);
492 : }
493 1617 : }
494 1617 : if (git_packbuilder_insert_walk(pb.get(), walker.get()) != 0) {
495 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to insert commits for {}",
496 : accountId_,
497 : repositoryId_,
498 : fmt::ptr(this),
499 : repository_);
500 0 : return;
501 : }
502 :
503 1617 : git_buf data = {};
504 1617 : if (git_packbuilder_write_buf(&data, pb.get()) != 0) {
505 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to write pack data for {}",
506 : accountId_,
507 : repositoryId_,
508 : fmt::ptr(this),
509 : repository_);
510 0 : return;
511 : }
512 :
513 1617 : std::size_t sent = 0;
514 1617 : std::size_t len = data.size;
515 1617 : std::error_code ec;
516 1617 : std::vector<uint8_t> toSendData;
517 : do {
518 : // cf https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L166
519 : // In 'side-band-64k' mode it will send up to 65519 data bytes plus 1 control code, for a
520 : // total of up to 65520 bytes in a pkt-line.
521 1637 : std::size_t pkt_size = std::min(static_cast<std::size_t>(65515), len - sent);
522 1637 : std::string toSendHeader = toGitHex(pkt_size + 5);
523 1637 : toSendData.clear();
524 1637 : toSendData.reserve(pkt_size + 5);
525 1637 : toSendData.insert(toSendData.end(), toSendHeader.begin(), toSendHeader.end());
526 1637 : toSendData.push_back(0x1);
527 1637 : toSendData.insert(toSendData.end(), data.ptr + sent, data.ptr + sent + pkt_size);
528 :
529 1637 : socket_->write(reinterpret_cast<const unsigned char*>(toSendData.data()), toSendData.size(), ec);
530 1637 : if (ec) {
531 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to send data for {}: {}",
532 : accountId_,
533 : repositoryId_,
534 : fmt::ptr(this),
535 : repository_,
536 : ec.message());
537 0 : git_buf_dispose(&data);
538 0 : return;
539 : }
540 1637 : sent += pkt_size;
541 3274 : } while (sent < len);
542 1617 : git_buf_dispose(&data);
543 1617 : toSendData = {};
544 :
545 : // And finish by a little FLUSH
546 1617 : socket_->write(reinterpret_cast<const uint8_t*>(FLUSH_PKT.data()), FLUSH_PKT.size(), ec);
547 1617 : if (ec) {
548 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to send data for {}: {}",
549 : accountId_,
550 : repositoryId_,
551 : fmt::ptr(this),
552 : repository_,
553 : ec.message());
554 : }
555 :
556 : // Clear sent data
557 1617 : haveRefs_.clear();
558 1617 : wantedReference_.clear();
559 1617 : common_.clear();
560 1617 : if (onFetchedCb_)
561 1615 : onFetchedCb_(fetched);
562 1617 : }
563 :
564 : std::map<std::string, std::string>
565 2250 : GitServer::Impl::getParameters(std::string_view pkt_line)
566 : {
567 2250 : std::map<std::string, std::string> parameters;
568 2250 : std::string key, value;
569 2250 : auto isKey = true;
570 2250 : auto nullChar = 0;
571 254250 : for (auto letter : pkt_line) {
572 252000 : if (letter == '\0') {
573 : // parameters such as host or version are after the first \0
574 4500 : if (nullChar != 0 && !key.empty()) {
575 2250 : parameters.try_emplace(std::move(key), std::move(value));
576 : }
577 4500 : nullChar += 1;
578 4500 : isKey = true;
579 4500 : key.clear();
580 4500 : value.clear();
581 247500 : } else if (letter == '=') {
582 2250 : isKey = false;
583 245250 : } else if (nullChar != 0) {
584 153000 : if (isKey) {
585 9000 : key += letter;
586 : } else {
587 144000 : value += letter;
588 : }
589 : }
590 : }
591 4500 : return parameters;
592 2250 : }
593 :
594 1529 : GitServer::GitServer(const std::string& accountId,
595 : const std::string& conversationId,
596 1529 : const std::shared_ptr<dhtnet::ChannelSocketInterface>& client)
597 : {
598 1529 : auto path = (fileutils::get_data_dir() / accountId / "conversations" / conversationId).string();
599 1529 : pimpl_ = std::make_unique<GitServer::Impl>(accountId, conversationId, path, client);
600 1529 : }
601 :
602 1529 : GitServer::~GitServer()
603 : {
604 1529 : stop();
605 1529 : pimpl_.reset();
606 1529 : }
607 :
608 : void
609 1524 : GitServer::setOnFetched(const onFetchedCb& cb)
610 : {
611 1524 : if (!pimpl_)
612 0 : return;
613 1524 : pimpl_->onFetchedCb_ = cb;
614 : }
615 :
616 : void
617 2256 : GitServer::stop()
618 : {
619 2256 : pimpl_->stop();
620 2256 : }
621 :
622 : } // namespace jami
|