• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kimci86 / bkcrack / 18822410056

26 Oct 2025 07:02PM UTC coverage: 85.064% (+4.7%) from 80.337%
18822410056

push

github

kimci86
Add unit tests for Arguments class

1720 of 2022 relevant lines covered (85.06%)

2884910.06 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.05
/src/cli/Arguments.cpp
1
#include "Arguments.hpp"
2

3
#include <bkcrack/Zip.hpp>
4
#include <bkcrack/file.hpp>
5

6
#include <algorithm>
7
#include <bitset>
8
#include <thread>
9
#include <type_traits>
10
#include <variant>
11

12
namespace
13
{
14

15
auto charRange(std::uint8_t first, std::uint8_t last) -> std::bitset<256>
528✔
16
{
17
    auto bitset = std::bitset<256>{};
1,584✔
18

19
    do
20
    {
21
        bitset.set(first);
36,432✔
22
    } while (first++ != last);
36,432✔
23

24
    return bitset;
528✔
25
}
26

27
auto bitsetToVector(const std::bitset<256>& charset) -> std::vector<std::uint8_t>
89✔
28
{
29
    auto vector = std::vector<std::uint8_t>{};
89✔
30
    for (auto c = 0; c < 256; c++)
22,873✔
31
        if (charset[c])
22,784✔
32
            vector.push_back(c);
3,306✔
33

34
    return vector;
89✔
35
}
×
36

37
template <typename F>
38
auto translateIntParseError(F&& f, const std::string& value)
88✔
39
{
40
    try
41
    {
42
        return f(value);
88✔
43
    }
44
    catch (const std::invalid_argument&)
6✔
45
    {
46
        throw Arguments::Error{"expected an integer, got \"" + value + "\""};
2✔
47
    }
48
    catch (const std::out_of_range&)
4✔
49
    {
50
        throw Arguments::Error{"integer value " + value + " is out of range"};
2✔
51
    }
52
}
53

54
auto parseInt(const std::string& value) -> int
14✔
55
{
56
    return translateIntParseError([](const std::string& value) { return std::stoi(value, nullptr, 0); }, value);
28✔
57
}
58

59
auto parseSize(const std::string& value) -> std::size_t
30✔
60
{
61
    return translateIntParseError([](const std::string& value) { return std::stoull(value, nullptr, 0); }, value);
60✔
62
}
63

64
auto parseInterval(const std::string& value) -> std::variant<Arguments::LengthInterval, std::size_t>
21✔
65
{
66
    const auto separator = std::string{".."};
21✔
67

68
    if (const auto minEnd = value.find(separator); minEnd != std::string::npos)
21✔
69
    {
70
        auto interval = Arguments::LengthInterval{};
6✔
71

72
        if (0 < minEnd)
6✔
73
            interval.minLength = parseSize(value.substr(0, minEnd));
4✔
74

75
        if (const auto maxBegin = minEnd + separator.size(); maxBegin < value.size())
6✔
76
            interval.maxLength = parseSize(value.substr(maxBegin));
4✔
77

78
        return interval;
6✔
79
    }
80
    else
81
        return parseSize(value);
15✔
82
}
21✔
83

84
} // namespace
85

86
Arguments::Error::Error(const std::string& description)
28✔
87
: BaseError{"Arguments error", description}
56✔
88
{
89
}
28✔
90

91
Arguments::Arguments(int argc, const char* const argv[])
88✔
92
: jobs{[]() -> int
176✔
93
       {
94
           const auto concurrency = std::thread::hardware_concurrency();
88✔
95
           return concurrency ? concurrency : 2;
88✔
96
       }()}
88✔
97
, m_current{argv + 1}
88✔
98
, m_end{argv + argc}
88✔
99
, m_charsets{
88✔
100
      []
×
101
      {
102
          const auto lowercase    = charRange('a', 'z');
88✔
103
          const auto uppercase    = charRange('A', 'Z');
88✔
104
          const auto digits       = charRange('0', '9');
88✔
105
          const auto alphanum     = lowercase | uppercase | digits;
88✔
106
          const auto printable    = charRange(' ', '~');
88✔
107
          const auto punctuation  = printable & ~alphanum;
88✔
108
          const auto bytes        = charRange('\x00', '\xff');
88✔
109
          const auto questionMark = charRange('?', '?');
88✔
110

111
          return std::unordered_map<char, std::bitset<256>>{
112
              {'l', lowercase}, {'u', uppercase},   {'d', digits}, {'a', alphanum},
×
113
              {'p', printable}, {'s', punctuation}, {'b', bytes},  {'?', questionMark},
×
114
          };
264✔
115
      }(),
116
  }
176✔
117
{
118
    // parse arguments
119
    while (!finished())
299✔
120
        parseArgument();
217✔
121

122
    if (help || version || infoArchive)
82✔
123
        return; // no further checks are needed for those options
7✔
124

125
    // deferred computations
126
    if (m_rawBruteforce)
75✔
127
        bruteforce = bitsetToVector(resolveCharset(*m_rawBruteforce));
25✔
128
    if (m_rawMask)
71✔
129
    {
130
        mask.emplace();
13✔
131
        for (auto it = m_rawMask->begin(); it != m_rawMask->end(); ++it)
185✔
132
        {
133
            if (*it == '?') // escape character to reference other charsets
172✔
134
            {
135
                if (++it == m_rawMask->end())
68✔
136
                {
137
                    mask->push_back({'?'});
×
138
                    break;
×
139
                }
140

141
                mask->push_back(bitsetToVector(resolveCharset(std::string{"?"} + *it)));
204✔
142
            }
143
            else
144
                mask->push_back({static_cast<std::uint8_t>(*it)});
312✔
145
        }
146
    }
147

148
    // check constraints on arguments
149
    if (keys)
71✔
150
    {
151
        if (!decipheredFile && !decryptedArchive && !changePassword && !changeKeys && !bruteforce && !mask)
44✔
152
            throw Error{"-d, -D, -U, --change-keys, --bruteforce or --mask parameter is missing (required by -k)"};
3✔
153
    }
154
    else if (!password)
27✔
155
    {
156
        if (cipherFile && cipherIndex)
21✔
157
            throw Error{"-c and --cipher-index cannot be used at the same time"};
3✔
158
        if (plainFile && plainIndex)
20✔
159
            throw Error{"-p and --plain-index cannot be used at the same time"};
3✔
160

161
        if (!cipherFile && !cipherIndex)
19✔
162
            throw Error{"-c or --cipher-index parameter is missing"};
6✔
163
        if (!plainFile && !plainIndex && extraPlaintext.empty())
17✔
164
            throw Error{"-p, --plain-index or -x parameter is missing"};
3✔
165

166
        if (plainArchive && !plainFile && !plainIndex)
16✔
167
            throw Error{"-p or --plain-index parameter is missing (required by -P)"};
3✔
168

169
        if (cipherIndex && !cipherArchive)
15✔
170
            throw Error{"-C parameter is missing (required by --cipher-index)"};
3✔
171
        if (plainIndex && !plainArchive)
14✔
172
            throw Error{"-P parameter is missing (required by --plain-index)"};
3✔
173

174
        constexpr auto minimumOffset = -static_cast<int>(Data::encryptionHeaderSize);
13✔
175
        if (offset < minimumOffset)
13✔
176
            throw Error{"plaintext offset " + std::to_string(offset) + " is too small (minimum is " +
2✔
177
                        std::to_string(minimumOffset) + ")"};
3✔
178
        if (!extraPlaintext.empty() && extraPlaintext.begin()->first < minimumOffset)
12✔
179
            throw Error{"extra plaintext offset " + std::to_string(extraPlaintext.begin()->first) +
2✔
180
                        " is too small (minimum is " + std::to_string(minimumOffset) + ")"};
3✔
181
    }
182

183
    if (decipheredFile && !cipherFile && !cipherIndex)
60✔
184
        throw Error{"-c or --cipher-index parameter is missing (required by -d)"};
3✔
185
    if (decipheredFile && !cipherArchive && decipheredFile == cipherFile)
59✔
186
        throw Error{"-c and -d parameters must point to different files"};
×
187

188
    if (decryptedArchive && !cipherArchive)
59✔
189
        throw Error{"-C parameter is missing (required by -D)"};
3✔
190
    if (decryptedArchive && decryptedArchive == cipherArchive)
58✔
191
        throw Error{"-C and -D parameters must point to different files"};
×
192

193
    if (changePassword && !cipherArchive)
58✔
194
        throw Error{"-C parameter is missing (required by -U)"};
3✔
195
    if (changePassword && changePassword->unlockedArchive == cipherArchive)
57✔
196
        throw Error{"-C and -U parameters must point to different files"};
×
197

198
    if (changeKeys && !cipherArchive)
57✔
199
        throw Error{"-C parameter is missing (required by --change-keys)"};
3✔
200
    if (changeKeys && changeKeys->unlockedArchive == cipherArchive)
56✔
201
        throw Error{"-C and --change-keys parameters must point to different files"};
×
202

203
    if (length && !bruteforce)
56✔
204
        throw Error{"--bruteforce parameter is missing (required by --length)"};
3✔
205

206
    if (bruteforce && mask)
55✔
207
        throw Error{"--bruteforce and --mask cannot be used at the same time"};
3✔
208
}
486✔
209

210
auto Arguments::loadData() const -> Data
1✔
211
{
212
    // load known plaintext
213
    auto plaintext = std::vector<std::uint8_t>{};
1✔
214
    if (plainArchive)
1✔
215
    {
216
        auto       stream  = openInput(*plainArchive);
×
217
        const auto archive = Zip{stream};
×
218
        const auto entry   = plainFile ? archive[*plainFile] : archive[*plainIndex];
×
219
        Zip::checkEncryption(entry, Zip::Encryption::None);
×
220
        plaintext = archive.load(entry, plainFilePrefix);
×
221
    }
×
222
    else if (plainFile)
1✔
223
    {
224
        auto stream = openInput(*plainFile);
×
225
        plaintext   = loadStream(stream, plainFilePrefix);
×
226
    }
×
227

228
    // load ciphertext needed by the attack
229
    auto needed = Data::encryptionHeaderSize;
1✔
230
    if (!plaintext.empty())
1✔
231
        needed = std::max(needed, Data::encryptionHeaderSize + offset + plaintext.size());
×
232
    if (!extraPlaintext.empty())
1✔
233
        needed = std::max(needed, Data::encryptionHeaderSize + extraPlaintext.rbegin()->first + 1);
1✔
234

235
    auto ciphertext                  = std::vector<std::uint8_t>{};
1✔
236
    auto extraPlaintextWithCheckByte = std::optional<std::map<int, std::uint8_t>>{};
1✔
237
    if (cipherArchive)
1✔
238
    {
239
        auto       stream  = openInput(*cipherArchive);
1✔
240
        const auto archive = Zip{stream};
1✔
241
        const auto entry   = cipherFile ? archive[*cipherFile] : archive[*cipherIndex];
1✔
242
        Zip::checkEncryption(entry, Zip::Encryption::Traditional);
1✔
243
        ciphertext = archive.load(entry, needed);
1✔
244

245
        if (!ignoreCheckByte && !extraPlaintext.count(-1))
1✔
246
        {
247
            extraPlaintextWithCheckByte        = extraPlaintext;
1✔
248
            (*extraPlaintextWithCheckByte)[-1] = entry.checkByte;
1✔
249
        }
250
    }
1✔
251
    else
252
    {
253
        auto stream = openInput(*cipherFile);
×
254
        ciphertext  = loadStream(stream, needed);
×
255
    }
×
256

257
    return {std::move(ciphertext), std::move(plaintext), offset, extraPlaintextWithCheckByte.value_or(extraPlaintext)};
2✔
258
}
1✔
259

260
auto Arguments::LengthInterval::operator&(const Arguments::LengthInterval& other) const -> Arguments::LengthInterval
21✔
261
{
262
    return {std::max(minLength, other.minLength), std::min(maxLength, other.maxLength)};
21✔
263
}
264

265
auto Arguments::resolveCharset(const std::string& rawCharset) -> std::bitset<256>
111✔
266
{
267
    auto charset = std::bitset<256>{};
333✔
268

269
    for (auto it = rawCharset.begin(); it != rawCharset.end(); ++it)
226✔
270
    {
271
        if (*it == '?') // escape character to reference other charsets
129✔
272
        {
273
            if (++it == rawCharset.end())
111✔
274
            {
275
                charset.set('?');
1✔
276
                break;
1✔
277
            }
278

279
            if (const auto rawCharsetsIt = m_rawCharsets.find(*it); rawCharsetsIt != m_rawCharsets.end())
110✔
280
            {
281
                // insert into m_charsets first to mark the identifier is being resolved and detect cycles
282
                if (const auto [charsetsIt, inserted] = m_charsets.try_emplace(*it); inserted)
21✔
283
                {
284
                    charsetsIt->second = resolveCharset(rawCharsetsIt->second);
18✔
285
                    m_rawCharsets.erase(rawCharsetsIt);
9✔
286
                }
287
                else
288
                    throw Error{std::string{"circular reference resolving charset ?"} + *it};
9✔
289
            }
290

291
            if (const auto charsetsIt = m_charsets.find(*it); charsetsIt != m_charsets.end())
98✔
292
                charset |= charsetsIt->second;
97✔
293
            else
294
                throw Error{std::string{"unknown charset ?"} + *it};
3✔
295
        }
296
        else
297
            charset.set(*it);
18✔
298
    }
299

300
    return charset;
98✔
301
}
302

303
auto Arguments::finished() const -> bool
870✔
304
{
305
    return m_current == m_end;
870✔
306
}
307

308
void Arguments::parseArgument()
217✔
309
{
310
    switch (readOption("an option"))
434✔
311
    {
312
    case Option::cipherFile:
25✔
313
        cipherFile = readString("ciphertext");
25✔
314
        break;
25✔
315
    case Option::cipherIndex:
3✔
316
        cipherIndex = readSize("index");
3✔
317
        break;
3✔
318
    case Option::cipherArchive:
10✔
319
        cipherArchive = readString("encryptedzip");
10✔
320
        break;
10✔
321
    case Option::plainFile:
16✔
322
        plainFile = readString("plaintext");
16✔
323
        break;
16✔
324
    case Option::plainIndex:
3✔
325
        plainIndex = readSize("index");
3✔
326
        break;
3✔
327
    case Option::plainArchive:
3✔
328
        plainArchive = readString("plainzip");
3✔
329
        break;
3✔
330
    case Option::plainFilePrefix:
1✔
331
        plainFilePrefix = readSize("size");
1✔
332
        break;
1✔
333
    case Option::offset:
4✔
334
        offset = readInt("offset");
6✔
335
        break;
2✔
336
    case Option::extraPlaintext:
7✔
337
    {
338
        auto i = readInt("offset");
14✔
339
        for (const auto b : readHex("data"))
44✔
340
            extraPlaintext[i++] = b;
35✔
341
        break;
5✔
342
    }
343
    case Option::ignoreCheckByte:
1✔
344
        ignoreCheckByte = true;
1✔
345
        break;
1✔
346
    case Option::attackStart:
2✔
347
        attackStart = readInt("checkpoint");
2✔
348
        break;
2✔
349
    case Option::password:
8✔
350
        password = readString("password");
8✔
351
        break;
8✔
352
    case Option::keys:
48✔
353
        keys = {readKey("X"), readKey("Y"), readKey("Z")};
234✔
354
        break;
46✔
355
    case Option::decipheredFile:
5✔
356
        decipheredFile = readString("decipheredfile");
5✔
357
        break;
5✔
358
    case Option::keepHeader:
1✔
359
        keepHeader = true;
1✔
360
        break;
1✔
361
    case Option::decryptedArchive:
3✔
362
        decryptedArchive = readString("decipheredzip");
3✔
363
        break;
3✔
364
    case Option::changePassword:
3✔
365
        changePassword = {readString("unlockedzip"), readString("password")};
3✔
366
        break;
3✔
367
    case Option::changeKeys:
3✔
368
        changeKeys = {readString("unlockedzip"), {readKey("X"), readKey("Y"), readKey("Z")}};
3✔
369
        break;
3✔
370
    case Option::bruteforce:
9✔
371
        m_rawBruteforce = readRawCharset("charset for bruteforce password recovery");
9✔
372
        break;
9✔
373
    case Option::length:
5✔
374
        length = length.value_or(LengthInterval{}) &
5✔
375
                 std::visit(
5✔
376
                     [](auto arg)
10✔
377
                     {
378
                         if constexpr (std::is_same_v<decltype(arg), std::size_t>)
379
                             return LengthInterval{arg, arg}; // a single value is interpreted as an exact length
4✔
380
                         else
381
                             return arg;
6✔
382
                     },
383
                     parseInterval(readString("length")));
10✔
384
        break;
5✔
385
    case Option::recoverPassword:
16✔
386
        length = length.value_or(LengthInterval{}) &
16✔
387
                 std::visit(
16✔
388
                     [](auto arg)
32✔
389
                     {
390
                         if constexpr (std::is_same_v<decltype(arg), std::size_t>)
391
                             return LengthInterval{0, arg}; // a single value is interpreted as an interval 0..max
26✔
392
                         else
393
                             return arg;
6✔
394
                     },
395
                     parseInterval(readString("length")));
48✔
396
        m_rawBruteforce = readRawCharset("charset for bruteforce password recovery");
16✔
397
        break;
16✔
398
    case Option::mask:
13✔
399
        m_rawMask = readString("mask");
13✔
400
        break;
13✔
401
    case Option::charset:
18✔
402
    {
403
        const auto identifier = readString("identifier");
18✔
404
        if (identifier.size() != 1)
18✔
405
            throw Error{"charset identifier must be a single character, got \"" + identifier + "\""};
×
406
        if (m_charsets.count(identifier[0]) || m_rawCharsets.count(identifier[0]))
18✔
407
            throw Error{"charset ?" + identifier + " is already defined, it cannot be redefined"};
×
408
        m_rawCharsets[identifier[0]] = readRawCharset("charset ?" + identifier);
18✔
409
        break;
18✔
410
    }
18✔
411
    case Option::recoveryStart:
1✔
412
    {
413
        const auto checkpoint = readHex("checkpoint");
1✔
414
        recoveryStart.assign(checkpoint.begin(), checkpoint.end());
1✔
415
        break;
1✔
416
    }
1✔
417
    case Option::jobs:
1✔
418
        jobs = readInt("count");
1✔
419
        break;
1✔
420
    case Option::exhaustive:
1✔
421
        exhaustive = true;
1✔
422
        break;
1✔
423
    case Option::infoArchive:
2✔
424
        infoArchive = readString("zipfile");
2✔
425
        break;
2✔
426
    case Option::version:
2✔
427
        version = true;
2✔
428
        break;
2✔
429
    case Option::help:
3✔
430
        help = true;
3✔
431
        break;
3✔
432
    }
433
}
247✔
434

435
auto Arguments::readString(const std::string& description) -> std::string
571✔
436
{
437
    if (finished())
571✔
438
        throw Error{"expected " + description + ", got nothing"};
×
439

440
    return *m_current++;
1,142✔
441
}
442

443
auto Arguments::readOption(const std::string& description) -> Arguments::Option
217✔
444
{
445
    // clang-format off
446
#define PAIR(string, option) {#string, Option::option}
447
#define PAIRS(short, long, option) PAIR(short, option), PAIR(long, option)
448

449
    // GCOVR_EXCL_START
450
    static const auto stringToOption = std::map<std::string, Option>{
451
        PAIRS(-c, --cipher-file,       cipherFile),
452
        PAIR (    --cipher-index,      cipherIndex),
453
        PAIRS(-C, --cipher-zip,        cipherArchive),
454
        PAIRS(-p, --plain-file,        plainFile),
455
        PAIR (    --plain-index,       plainIndex),
456
        PAIRS(-P, --plain-zip,         plainArchive),
457
        PAIRS(-t, --truncate,          plainFilePrefix),
458
        PAIRS(-o, --offset,            offset),
459
        PAIRS(-x, --extra,             extraPlaintext),
460
        PAIR (    --ignore-check-byte, ignoreCheckByte),
461
        PAIR (    --continue-attack,   attackStart),
462
        PAIR (    --password,          password),
463
        PAIRS(-k, --keys,              keys),
464
        PAIRS(-d, --decipher,          decipheredFile),
465
        PAIR (    --keep-header,       keepHeader),
466
        PAIRS(-D, --decrypt,           decryptedArchive),
467
        PAIRS(-U, --change-password,   changePassword),
468
        PAIR (    --change-keys,       changeKeys),
469
        PAIRS(-b, --bruteforce,        bruteforce),
470
        PAIRS(-l, --length,            length),
471
        PAIRS(-r, --recover-password,  recoverPassword),
472
        PAIRS(-m, --mask,              mask),
473
        PAIRS(-s, --charset,           charset),
474
        PAIR (    --continue-recovery, recoveryStart),
475
        PAIRS(-j, --jobs,              jobs),
476
        PAIRS(-e, --exhaustive,        exhaustive),
477
        PAIRS(-L, --list,              infoArchive),
478
        PAIR (    --version,           version),
479
        PAIRS(-h, --help,              help),
480
    };
481
    // GCOVR_EXCL_STOP
482
    // clang-format on
483

484
#undef PAIR
485
#undef PAIRS
486

487
    const auto str = readString(description);
217✔
488
    if (const auto it = stringToOption.find(str); it == stringToOption.end())
217✔
489
        throw Error{"unknown option " + str};
×
490
    else
491
        return it->second;
434✔
492
}
249✔
493

494
auto Arguments::readInt(const std::string& description) -> int
14✔
495
{
496
    return parseInt(readString(description));
14✔
497
}
498

499
auto Arguments::readSize(const std::string& description) -> std::size_t
7✔
500
{
501
    return parseSize(readString(description));
7✔
502
}
503

504
auto Arguments::readHex(const std::string& description) -> std::vector<std::uint8_t>
8✔
505
{
506
    const auto str = readString(description);
8✔
507

508
    if (str.size() % 2)
8✔
509
        throw Error{"expected an even-length string, got " + str};
1✔
510
    if (!std::all_of(str.begin(), str.end(), [](char c) { return std::isxdigit(static_cast<unsigned char>(c)); }))
74✔
511
        throw Error{"expected " + description + " in hexadecimal, got " + str};
1✔
512

513
    auto data = std::vector<std::uint8_t>{};
6✔
514
    for (auto i = std::size_t{}; i < str.length(); i += 2)
39✔
515
        data.push_back(static_cast<std::uint8_t>(std::stoul(str.substr(i, 2), nullptr, 16)));
33✔
516

517
    return data;
12✔
518
}
8✔
519

520
auto Arguments::readKey(const std::string& description) -> std::uint32_t
149✔
521
{
522
    const auto str = readString(description);
149✔
523

524
    if (str.size() > 8)
149✔
525
        throw Error{"expected a string of length 8 or less, got " + str};
1✔
526
    if (!std::all_of(str.begin(), str.end(), [](char c) { return std::isxdigit(static_cast<unsigned char>(c)); }))
827✔
527
        throw Error{"expected " + description + " in hexadecimal, got " + str};
1✔
528

529
    return static_cast<std::uint32_t>(std::stoul(str, nullptr, 16));
294✔
530
}
149✔
531

532
auto Arguments::readRawCharset(const std::string& description) -> std::string
43✔
533
{
534
    auto charset = readString(description);
43✔
535

536
    if (charset.empty())
43✔
537
        throw Error{description + " is empty"};
×
538

539
    return charset;
43✔
540
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc