Celestia

Форк
0
/
starname.cpp 
623 строки · 20.8 Кб
1
//
2
// C++ Implementation: starname
3
//
4
// Description:
5
//
6
//
7
// Author: Toti <root@totibox>, (C) 2005
8
//
9
// Copyright: See COPYING file that comes with this distribution
10
//
11
//
12

13
#include "starname.h"
14

15
#include <algorithm>
16
#include <cassert>
17
#include <cctype>
18
#include <cstddef>
19
#include <istream>
20
#include <iterator>
21
#include <optional>
22
#include <string>
23
#include <system_error>
24
#include <type_traits>
25

26
#include <fmt/format.h>
27

28
#include <celcompat/charconv.h>
29
#include <celutil/binaryread.h>
30
#include <celutil/gettext.h>
31
#include <celutil/greek.h>
32
#include <celutil/logger.h>
33
#include <celutil/timer.h>
34
#include "astroobj.h"
35
#include "constellation.h"
36

37
using namespace std::string_view_literals;
38

39
namespace compat = celestia::compat;
40
namespace util = celestia::util;
41

42
using util::GetLogger;
43

44
namespace
45
{
46

47
constexpr std::string_view CROSSINDEX_MAGIC = "CELINDEX"sv;
48
constexpr std::uint16_t CrossIndexVersion   = 0x0100;
49

50
constexpr std::string_view HDCatalogPrefix        = "HD "sv;
51
constexpr std::string_view HIPPARCOSCatalogPrefix = "HIP "sv;
52
constexpr std::string_view TychoCatalogPrefix     = "TYC "sv;
53
constexpr std::string_view SAOCatalogPrefix       = "SAO "sv;
54

55
constexpr AstroCatalog::IndexNumber TYC123_MIN = 1u;
56
constexpr AstroCatalog::IndexNumber TYC1_MAX   = 9999u;  // actual upper limit is 9537 in TYC2
57
constexpr AstroCatalog::IndexNumber TYC2_MAX   = 99999u; // actual upper limit is 12121 in TYC2
58
constexpr AstroCatalog::IndexNumber TYC3_MAX   = 3u;     // from TYC2
59

60
// In the original Tycho catalog, TYC3 ranges from 1 to 3, so no there is
61
// no chance of overflow in the multiplication. TDSC (Fabricius et al. 2002)
62
// adds one entry with TYC3 = 4 (TYC 2907-1276-4) so permit TYC=4 when the
63
// TYC1 number is <= 2907
64
constexpr inline AstroCatalog::IndexNumber TDSC_TYC3_MAX            = 4u;
65
constexpr inline AstroCatalog::IndexNumber TDSC_TYC3_MAX_RANGE_TYC1 = 2907u;
66

67
#pragma pack(push, 1)
68

69
// cross-index header structure
70
struct CrossIndexHeader
71
{
72
    CrossIndexHeader() = delete;
73
    char magic[8]; //NOSONAR
74
    std::uint16_t version;
75
};
76

77
// cross-index record structure
78
struct CrossIndexRecord
79
{
80
    CrossIndexRecord() = delete;
81
    std::uint32_t catalogNumber;
82
    std::uint32_t celCatalogNumber;
83
};
84

85
#pragma pack(pop)
86

87
static_assert(std::is_standard_layout_v<CrossIndexHeader>);
88
static_assert(std::is_standard_layout_v<CrossIndexRecord>);
89

90
constexpr unsigned int FIRST_NUMBERED_VARIABLE = 335;
91

92
using CanonicalBuffer = fmt::basic_memory_buffer<char, 256>;
93

94
// workaround for missing append method in earlier fmt versions
95
inline void
96
appendComponentA(CanonicalBuffer& buffer)
97
{
98
    buffer.push_back(' ');
99
    buffer.push_back('A');
100
}
101

102
// Try parsing the first word of a name as a Flamsteed number or variable star
103
// designation. Single-letter variable star designations are handled by the
104
// Bayer parser due to indistinguishability with case-insensitive lookup.
105
bool
106
isFlamsteedOrVariable(std::string_view prefix)
107
{
108
    using compat::from_chars;
109
    switch (prefix.size())
110
    {
111
        case 0:
112
            return false;
113
        case 1:
114
            // Match single-digit Flamsteed number
115
            return prefix[0] >= '1' && prefix[0] <= '9';
116
        case 2:
117
            {
118
                auto p0 = static_cast<unsigned char>(prefix[0]);
119
                auto p1 = static_cast<unsigned char>(prefix[1]);
120
                return
121
                    // Two-digit Flamsteed number
122
                    (std::isdigit(p0) && p0 != '0' && std::isdigit(p1)) ||
123
                    (std::isalpha(p0) && std::isalpha(p1) &&
124
                     std::tolower(p0) != 'j' && std::tolower(p1) != 'j' &&
125
                     p1 >= p0);
126
            }
127
        default:
128
            {
129
                // check for either Flamsteed or V### format variable star designations
130
                std::size_t startNumber = std::tolower(static_cast<unsigned char>(prefix[0])) == 'v'
131
                    ? 1
132
                    : 0;
133
                auto endPtr = prefix.data() + prefix.size();
134
                unsigned int value;
135
                auto [ptr, ec] = from_chars(prefix.data() + startNumber, endPtr, value);
136
                return ec == std::errc{} && ptr == endPtr &&
137
                       (startNumber == 0 || value >= FIRST_NUMBERED_VARIABLE);
138
            }
139
    }
140
}
141

142
struct BayerLetter
143
{
144
    std::string_view letter{ };
145
    unsigned int number{ 0 };
146
};
147

148
// Attempts to parse the first word of a star name as a Greek or Latin-letter
149
// Bayer designation, with optional numeric suffix
150
BayerLetter
151
parseBayerLetter(std::string_view prefix)
152
{
153
    using compat::from_chars;
154

155
    BayerLetter result;
156
    if (auto numberPos = prefix.find_first_of("0123456789"); numberPos == std::string_view::npos)
157
        result.letter = prefix;
158
    else if (auto [ptr, ec] = from_chars(prefix.data() + numberPos, prefix.data() + prefix.size(), result.number);
159
             ec == std::errc{} && ptr == prefix.data() + prefix.size())
160
        result.letter = prefix.substr(0, numberPos);
161
    else
162
        return {};
163

164
    if (result.letter.empty())
165
        return {};
166

167
    if (auto greek = GetCanonicalGreekAbbreviation(result.letter); !greek.empty())
168
        result.letter = greek;
169
    else if (result.letter.size() != 1 || !std::isalpha(static_cast<unsigned char>(result.letter[0])))
170
        return {};
171

172
    return result;
173
}
174

175
bool
176
parseSimpleCatalogNumber(std::string_view name,
177
                         std::string_view prefix,
178
                         AstroCatalog::IndexNumber& catalogNumber)
179
{
180
    using compat::from_chars;
181
    if (compareIgnoringCase(name, prefix, prefix.size()) != 0)
182
        return false;
183

184
    // skip additional whitespace
185
    auto pos = name.find_first_not_of(" \t", prefix.size());
186
    if (pos == std::string_view::npos)
187
        return false;
188

189
    if (auto [ptr, ec] = from_chars(name.data() + pos, name.data() + name.size(), catalogNumber); ec == std::errc{})
190
    {
191
        // Do not match if suffix is present
192
        pos = name.find_first_not_of(" \t", ptr - name.data());
193
        return pos == std::string_view::npos;
194
    }
195

196
    return false;
197
}
198

199
bool
200
parseTychoCatalogNumber(std::string_view name,
201
                        AstroCatalog::IndexNumber& catalogNumber)
202
{
203
    using compat::from_chars;
204
    if (compareIgnoringCase(name, TychoCatalogPrefix, TychoCatalogPrefix.size()) != 0)
205
        return false;
206

207
    // skip additional whitespace
208
    auto pos = name.find_first_not_of(" \t", TychoCatalogPrefix.size());
209
    if (pos == std::string_view::npos)
210
        return false;
211

212
    const char* const end_ptr = name.data() + name.size();
213

214
    std::array<AstroCatalog::IndexNumber, 3> tycParts;
215
    auto result = from_chars(name.data() + pos, end_ptr, tycParts[0]);
216
    if (result.ec != std::errc{}
217
        || tycParts[0] < TYC123_MIN || tycParts[0] > TYC1_MAX
218
        || result.ptr == end_ptr
219
        || *result.ptr != '-')
220
    {
221
        return false;
222
    }
223

224
    result = from_chars(result.ptr + 1, end_ptr, tycParts[1]);
225
    if (result.ec != std::errc{}
226
        || tycParts[1] < TYC123_MIN || tycParts[1] > TYC2_MAX
227
        || result.ptr == end_ptr
228
        || *result.ptr != '-')
229
    {
230
        return false;
231
    }
232

233
    if (result = from_chars(result.ptr + 1, end_ptr, tycParts[2]);
234
        result.ec == std::errc{}
235
        && tycParts[2] >= TYC123_MIN
236
        && (tycParts[2] <= TYC3_MAX
237
            || (tycParts[2] == TDSC_TYC3_MAX && tycParts[0] <= TDSC_TYC3_MAX_RANGE_TYC1)))
238
    {
239
        // Do not match if suffix is present
240
        pos = name.find_first_not_of(" \t", result.ptr - name.data());
241
        if (pos != std::string_view::npos)
242
            return false;
243

244
        catalogNumber = tycParts[2] * StarNameDatabase::TYC3_MULTIPLIER
245
                      + tycParts[1] * StarNameDatabase::TYC2_MULTIPLIER
246
                      + tycParts[0];
247
        return true;
248
    }
249

250
    return false;
251
}
252

253
bool
254
parseCelestiaCatalogNumber(std::string_view name,
255
                           AstroCatalog::IndexNumber& catalogNumber)
256
{
257
    using celestia::compat::from_chars;
258
    if (name.size() == 0 || name[0] != '#')
259
        return false;
260

261
    if (auto [ptr, ec] = from_chars(name.data() + 1, name.data() + name.size(), catalogNumber);
262
        ec == std::errc{})
263
    {
264
        // Do not match if suffix is present
265
        auto pos = name.find_first_not_of(" \t", ptr - name.data());
266
        return pos == std::string_view::npos;
267
    }
268

269
    return false;
270
}
271

272
// Verify that the cross index file has a correct header
273
bool
274
checkCrossIndexHeader(std::istream& in)
275
{
276
    std::array<char, sizeof(CrossIndexHeader)> header;
277
    if (!in.read(header.data(), header.size()).good()) /* Flawfinder: ignore */
278
        return false;
279

280
    // Verify the magic string
281
    if (std::string_view(header.data() + offsetof(CrossIndexHeader, magic), CROSSINDEX_MAGIC.size()) != CROSSINDEX_MAGIC)
282
    {
283
        GetLogger()->error(_("Bad header for cross index\n"));
284
        return false;
285
    }
286

287
    // Verify the version
288
    if (auto version = util::fromMemoryLE<std::uint16_t>(header.data() + offsetof(CrossIndexHeader, version));
289
        version != CrossIndexVersion)
290
    {
291
        GetLogger()->error(_("Bad version for cross index\n"));
292
        return false;
293
    }
294

295
    return true;
296
}
297

298
} // end unnamed namespace
299

300
AstroCatalog::IndexNumber
301
StarNameDatabase::findCatalogNumberByName(std::string_view name, bool i18n) const
302
{
303
    if (name.empty())
304
        return AstroCatalog::InvalidIndex;
305

306
    AstroCatalog::IndexNumber catalogNumber = findByName(name, i18n);
307
    if (catalogNumber != AstroCatalog::InvalidIndex)
308
        return catalogNumber;
309

310
    if (parseCelestiaCatalogNumber(name, catalogNumber))
311
        return catalogNumber;
312
    if (parseSimpleCatalogNumber(name, HIPPARCOSCatalogPrefix, catalogNumber))
313
        return catalogNumber;
314
    if (parseTychoCatalogNumber(name, catalogNumber))
315
        return catalogNumber;
316
    if (parseSimpleCatalogNumber(name, HDCatalogPrefix, catalogNumber))
317
        return searchCrossIndexForCatalogNumber(StarCatalog::HenryDraper, catalogNumber);
318
    if (parseSimpleCatalogNumber(name, SAOCatalogPrefix, catalogNumber))
319
        return searchCrossIndexForCatalogNumber(StarCatalog::SAO, catalogNumber);
320

321
    return AstroCatalog::InvalidIndex;
322
}
323

324
// Return the Celestia catalog number for the star with a specified number
325
// in a cross index.
326
AstroCatalog::IndexNumber
327
StarNameDatabase::searchCrossIndexForCatalogNumber(StarCatalog catalog, AstroCatalog::IndexNumber number) const
328
{
329
    auto catalogIndex = static_cast<std::size_t>(catalog);
330
    if (catalogIndex >= crossIndices.size())
331
        return AstroCatalog::InvalidIndex;
332

333
    const CrossIndex& xindex = crossIndices[catalogIndex];
334
    auto iter = std::lower_bound(xindex.begin(), xindex.end(), number,
335
                                 [](const CrossIndexEntry& ent, AstroCatalog::IndexNumber n) { return ent.catalogNumber < n; });
336
    return iter == xindex.end() || iter->catalogNumber != number
337
        ? AstroCatalog::InvalidIndex
338
        : iter->celCatalogNumber;
339
}
340

341
AstroCatalog::IndexNumber
342
StarNameDatabase::crossIndex(StarCatalog catalog, AstroCatalog::IndexNumber celCatalogNumber) const
343
{
344
    auto catalogIndex = static_cast<std::size_t>(catalog);
345
    if (catalogIndex >= crossIndices.size())
346
        return AstroCatalog::InvalidIndex;
347

348
    const CrossIndex& xindex = crossIndices[catalogIndex];
349

350
    // A simple linear search.  We could store cross indices sorted by
351
    // both catalog numbers and trade memory for speed
352
    auto iter = std::find_if(xindex.begin(), xindex.end(),
353
                             [celCatalogNumber](const CrossIndexEntry& o) { return celCatalogNumber == o.celCatalogNumber; });
354
    return iter == xindex.end()
355
        ? AstroCatalog::InvalidIndex
356
        : iter->catalogNumber;
357
}
358

359
AstroCatalog::IndexNumber
360
StarNameDatabase::findByName(std::string_view name, bool i18n) const
361
{
362
    if (auto catalogNumber = getCatalogNumberByName(name, i18n);
363
        catalogNumber != AstroCatalog::InvalidIndex)
364
        return catalogNumber;
365

366
    if (auto pos = name.find(' '); pos != 0 && pos != std::string::npos && pos < name.size() - 1)
367
    {
368
        std::string_view prefix = name.substr(0, pos);
369
        std::string_view remainder = name.substr(pos + 1);
370

371
        if (auto catalogNumber = findFlamsteedOrVariable(prefix, remainder, i18n);
372
            catalogNumber != AstroCatalog::InvalidIndex)
373
            return catalogNumber;
374

375
        if (auto catalogNumber = findBayer(prefix, remainder, i18n);
376
            catalogNumber != AstroCatalog::InvalidIndex)
377
            return catalogNumber;
378
    }
379

380
    return findWithComponentSuffix(name, i18n);
381
}
382

383
AstroCatalog::IndexNumber
384
StarNameDatabase::findFlamsteedOrVariable(std::string_view prefix,
385
                                          std::string_view remainder,
386
                                          bool i18n) const
387
{
388
    if (!isFlamsteedOrVariable(prefix))
389
        return AstroCatalog::InvalidIndex;
390

391
    auto [constellationAbbrev, suffix] = ParseConstellation(remainder);
392
    if (constellationAbbrev.empty() || (!suffix.empty() && suffix.front() != ' '))
393
        return AstroCatalog::InvalidIndex;
394

395
    CanonicalBuffer canonical;
396
    fmt::format_to(std::back_inserter(canonical), "{} {}{}", prefix, constellationAbbrev, suffix);
397
    if (auto catalogNumber = getCatalogNumberByName({canonical.data(), canonical.size()}, i18n);
398
        catalogNumber != AstroCatalog::InvalidIndex)
399
    {
400
        return catalogNumber;
401
    }
402

403
    if (!suffix.empty())
404
        return AstroCatalog::InvalidIndex;
405

406
    // try appending " A"
407
    appendComponentA(canonical);
408
    return getCatalogNumberByName({canonical.data(), canonical.size()}, i18n);
409
}
410

411
AstroCatalog::IndexNumber
412
StarNameDatabase::findBayer(std::string_view prefix,
413
                            std::string_view remainder,
414
                            bool i18n) const
415
{
416
    auto bayerLetter = parseBayerLetter(prefix);
417
    if (bayerLetter.letter.empty())
418
        return AstroCatalog::InvalidIndex;
419

420
    auto [constellationAbbrev, suffix] = ParseConstellation(remainder);
421
    if (constellationAbbrev.empty() || (!suffix.empty() && suffix.front() != ' '))
422
        return AstroCatalog::InvalidIndex;
423

424
    return bayerLetter.number == 0
425
        ? findBayerNoNumber(bayerLetter.letter, constellationAbbrev, suffix, i18n)
426
        : findBayerWithNumber(bayerLetter.letter,
427
                              bayerLetter.number,
428
                              constellationAbbrev,
429
                              suffix,
430
                              i18n);
431
}
432

433
AstroCatalog::IndexNumber
434
StarNameDatabase::findBayerNoNumber(std::string_view letter,
435
                                    std::string_view constellationAbbrev,
436
                                    std::string_view suffix,
437
                                    bool i18n) const
438
{
439
    CanonicalBuffer canonical;
440
    fmt::format_to(std::back_inserter(canonical), "{} {}{}", letter, constellationAbbrev, suffix);
441
    if (auto catalogNumber = getCatalogNumberByName({canonical.data(), canonical.size()}, i18n);
442
        catalogNumber != AstroCatalog::InvalidIndex)
443
    {
444
        return catalogNumber;
445
    }
446

447
    // Try appending "1" to the letter, e.g. ALF CVn --> ALF1 CVn
448
    canonical.clear();
449
    fmt::format_to(std::back_inserter(canonical), "{}1 {}{}", letter, constellationAbbrev, suffix);
450
    if (auto catalogNumber = getCatalogNumberByName({canonical.data(), canonical.size()}, i18n);
451
        catalogNumber != AstroCatalog::InvalidIndex)
452
    {
453
        return catalogNumber;
454
    }
455

456
    if (!suffix.empty())
457
        return AstroCatalog::InvalidIndex;
458

459
    // No component suffix, so try appending " A"
460
    canonical.clear();
461
    fmt::format_to(std::back_inserter(canonical), "{} {} A", letter, constellationAbbrev);
462
    if (auto catalogNumber = getCatalogNumberByName({canonical.data(), canonical.size()}, i18n);
463
        catalogNumber != AstroCatalog::InvalidIndex)
464
    {
465
        return catalogNumber;
466
    }
467

468
    // Try appending "1" to the letter and a, e.g. ALF CVn --> ALF1 CVn A
469
    if (auto [it, size] = fmt::format_to_n(canonical.data(), canonical.size(), "{}1 {} A",
470
                                           letter, constellationAbbrev);
471
        size <= canonical.size())
472
        return getCatalogNumberByName({canonical.data(), size}, i18n);
473

474
    return AstroCatalog::InvalidIndex;
475
}
476

477
AstroCatalog::IndexNumber
478
StarNameDatabase::findBayerWithNumber(std::string_view letter,
479
                                      unsigned int number,
480
                                      std::string_view constellationAbbrev,
481
                                      std::string_view suffix,
482
                                      bool i18n) const
483
{
484
    CanonicalBuffer canonical;
485
    fmt::format_to(std::back_inserter(canonical), "{}{} {}{}", letter, number, constellationAbbrev, suffix);
486
    if (auto catalogNumber = getCatalogNumberByName({canonical.data(), canonical.size()}, i18n);
487
        catalogNumber != AstroCatalog::InvalidIndex)
488
    {
489
        return catalogNumber;
490
    }
491

492
    if (!suffix.empty())
493
        return AstroCatalog::InvalidIndex;
494

495
    // No component suffix, so try appending " A"
496
    appendComponentA(canonical);
497
    return getCatalogNumberByName({canonical.data(), canonical.size()}, i18n);
498
}
499

500
AstroCatalog::IndexNumber
501
StarNameDatabase::findWithComponentSuffix(std::string_view name, bool i18n) const
502
{
503
    CanonicalBuffer canonical;
504
    fmt::format_to(std::back_inserter(canonical), "{} A", name);
505
    return getCatalogNumberByName({canonical.data(), canonical.size()}, i18n);
506
}
507

508
std::unique_ptr<StarNameDatabase>
509
StarNameDatabase::readNames(std::istream& in)
510
{
511
    using compat::from_chars;
512

513
    constexpr std::size_t maxLength = 1024;
514
    auto db = std::make_unique<StarNameDatabase>();
515
    std::string buffer(maxLength, '\0');
516
    while (!in.eof())
517
    {
518
        in.getline(buffer.data(), maxLength);
519
        // Delimiter is extracted and contributes to gcount() but is not stored
520
        std::size_t lineLength;
521

522
        if (in.good())
523
            lineLength = static_cast<std::size_t>(in.gcount() - 1);
524
        else if (in.eof())
525
            lineLength = static_cast<std::size_t>(in.gcount());
526
        else
527
            return nullptr;
528

529
        auto line = static_cast<std::string_view>(buffer).substr(0, lineLength);
530

531
        if (line.empty() || line.front() == '#')
532
            continue;
533

534
        auto pos = line.find(':');
535
        if (pos == std::string_view::npos)
536
            return nullptr;
537

538
        auto catalogNumber = AstroCatalog::InvalidIndex;
539
        if (auto [ptr, ec] = from_chars(line.data(), line.data() + pos, catalogNumber);
540
            ec != std::errc{} || ptr != line.data() + pos)
541
        {
542
            return nullptr;
543
        }
544

545
        // Iterate through the string for names delimited
546
        // by ':', and insert them into the star database. Note that
547
        // db->add() will skip empty names.
548
        line = line.substr(pos + 1);
549
        while (!line.empty())
550
        {
551
            pos = line.find(':');
552
            std::string_view name = line.substr(0, pos);
553
            db->add(catalogNumber, name);
554
            if (pos == std::string_view::npos)
555
                break;
556
            line = line.substr(pos + 1);
557
        }
558
    }
559

560
    return db;
561
}
562

563
bool
564
StarNameDatabase::loadCrossIndex(StarCatalog catalog, std::istream& in)
565
{
566
    Timer timer{};
567

568
    auto catalogIndex = static_cast<std::size_t>(catalog);
569
    if (catalogIndex >= crossIndices.size())
570
        return false;
571

572
    if (!checkCrossIndexHeader(in))
573
        return false;
574

575
    CrossIndex& xindex = crossIndices[catalogIndex];
576
    xindex = {};
577

578
    constexpr std::uint32_t BUFFER_RECORDS = UINT32_C(4096) / sizeof(CrossIndexRecord);
579
    std::vector<char> buffer(sizeof(CrossIndexRecord) * BUFFER_RECORDS);
580
    bool hasMoreRecords = true;
581
    while (hasMoreRecords)
582
    {
583
        std::size_t remainingRecords = BUFFER_RECORDS;
584
        in.read(buffer.data(), buffer.size()); /* Flawfinder: ignore */
585
        if (in.bad())
586
        {
587
            GetLogger()->error(_("Loading cross index failed\n"));
588
            xindex = {};
589
            return false;
590
        }
591
        if (in.eof())
592
        {
593
            auto bytesRead = static_cast<std::uint32_t>(in.gcount());
594
            remainingRecords = bytesRead / sizeof(CrossIndexRecord);
595
            // disallow partial records
596
            if (bytesRead % sizeof(CrossIndexRecord) != 0)
597
            {
598
                GetLogger()->error(_("Loading cross index failed - unexpected EOF\n"));
599
                xindex = {};
600
                return false;
601
            }
602

603
            hasMoreRecords = false;
604
        }
605

606
        xindex.reserve(xindex.size() + remainingRecords);
607

608
        const char* ptr = buffer.data();
609
        while (remainingRecords-- > 0)
610
        {
611
            CrossIndexEntry& ent = xindex.emplace_back();
612
            ent.catalogNumber = util::fromMemoryLE<AstroCatalog::IndexNumber>(ptr + offsetof(CrossIndexRecord, catalogNumber));
613
            ent.celCatalogNumber = util::fromMemoryLE<AstroCatalog::IndexNumber>(ptr + offsetof(CrossIndexRecord, celCatalogNumber));
614
            ptr += sizeof(CrossIndexRecord);
615
        }
616
    }
617

618
    GetLogger()->debug("Loaded xindex in {} ms\n", timer.getTime());
619

620
    std::sort(xindex.begin(), xindex.end(),
621
              [](const auto& lhs, const auto& rhs) { return lhs.catalogNumber < rhs.catalogNumber; });
622
    return true;
623
}
624

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.