Coverage Report

Created: 2026-09-01 06:25

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/crow/include/crow/query_string.h
Line
Count
Source
1
#pragma once
2
3
#include <stdio.h>
4
#include <string.h>
5
#include <string>
6
#include <vector>
7
#include <unordered_map>
8
#include <iostream>
9
#include <memory>
10
11
namespace crow
12
{
13
14
// ----------------------------------------------------------------------------
15
// qs_parse (modified)
16
// https://github.com/bartgrantham/qs_parse
17
// ----------------------------------------------------------------------------
18
/*  Similar to strncmp, but handles URL-encoding for either string  */
19
int qs_strncmp(const char* s, const char* qs, size_t n);
20
21
22
/*  Finds the beginning of each key/value pair and stores a pointer in qs_kv.
23
 *  Also decodes the value portion of the k/v pair *in-place*.  In a future
24
 *  enhancement it will also have a compile-time option of sorting qs_kv
25
 *  alphabetically by key.  */
26
size_t qs_parse(char* qs, char* qs_kv[], size_t qs_kv_size, bool parse_url);
27
28
29
/*  Used by qs_parse to decode the value portion of a k/v pair  */
30
int qs_decode(char * qs);
31
32
33
/*  Looks up the value according to the key on a pre-processed query string
34
 *  A future enhancement will be a compile-time option to look up the key
35
 *  in a pre-sorted qs_kv array via a binary search.  */
36
//char * qs_k2v(const char * key, char * qs_kv[], int qs_kv_size);
37
 char * qs_k2v(const char * key, char * const * qs_kv, size_t qs_kv_size, int nth);
38
39
40
/*  Non-destructive lookup of value, based on key.  User provides the
41
 *  destinaton string and length.  */
42
char * qs_scanvalue(const char * key, const char * qs, char * val, size_t val_len);
43
44
// TODO: implement sorting of the qs_kv array; for now ensure it's not compiled
45
#undef _qsSORTING
46
47
// isxdigit _is_ available in <ctype.h>, but let's avoid another header instead
48
#define CROW_QS_ISHEX(x)    ((((x)>='0'&&(x)<='9') || ((x)>='A'&&(x)<='F') || ((x)>='a'&&(x)<='f')) ? 1 : 0)
49
#define CROW_QS_HEX2DEC(x)  (((x)>='0'&&(x)<='9') ? (x)-48 : ((x)>='A'&&(x)<='F') ? (x)-55 : ((x)>='a'&&(x)<='f') ? (x)-87 : 0)
50
#define CROW_QS_ISQSCHR(x) ((((x)=='=')||((x)=='#')||((x)=='&')||((x)=='\0')) ? 0 : 1)
51
52
inline int qs_strncmp(const char * s, const char * qs, size_t n)
53
0
{
54
0
    unsigned char u1, u2, unyb, lnyb;
55
0
56
0
    while(n-- > 0)
57
0
    {
58
0
        u1 = static_cast<unsigned char>(*s++);
59
0
        u2 = static_cast<unsigned char>(*qs++);
60
0
61
0
        if ( ! CROW_QS_ISQSCHR(u1) ) {  u1 = '\0';  }
62
0
        if ( ! CROW_QS_ISQSCHR(u2) ) {  u2 = '\0';  }
63
0
64
0
        if ( u1 == '+' ) {  u1 = ' ';  }
65
0
        if ( u1 == '%' ) // easier/safer than scanf
66
0
        {
67
0
            // Check that next two chars exist and are valid hex before reading
68
0
            if ( CROW_QS_ISHEX(s[0]) && CROW_QS_ISHEX(s[1]) )
69
0
            {
70
0
                unyb = static_cast<unsigned char>(*s++);
71
0
                lnyb = static_cast<unsigned char>(*s++);
72
0
                u1 = (CROW_QS_HEX2DEC(unyb) * 16) + CROW_QS_HEX2DEC(lnyb);
73
0
            }
74
0
            else
75
0
            {
76
0
                u1 = '\0';
77
0
            }
78
0
        }
79
0
80
0
        if ( u2 == '+' ) {  u2 = ' ';  }
81
0
        if ( u2 == '%' ) // easier/safer than scanf
82
0
        {
83
0
            // Check that next two chars exist and are valid hex before reading
84
0
            if ( CROW_QS_ISHEX(qs[0]) && CROW_QS_ISHEX(qs[1]) )
85
0
            {
86
0
                unyb = static_cast<unsigned char>(*qs++);
87
0
                lnyb = static_cast<unsigned char>(*qs++);
88
0
                u2 = (CROW_QS_HEX2DEC(unyb) * 16) + CROW_QS_HEX2DEC(lnyb);
89
0
            }
90
0
            else
91
0
            {
92
0
                u2 = '\0';
93
0
            }
94
0
        }
95
0
96
0
        if ( u1 != u2 )
97
0
            return u1 - u2;
98
0
        if ( u1 == '\0' )
99
0
            return 0;
100
0
    }
101
0
    if ( CROW_QS_ISQSCHR(*qs) )
102
0
        return -1;
103
0
    else
104
0
        return 0;
105
0
}
106
107
108
inline size_t qs_parse(char* qs, char* qs_kv[], size_t qs_kv_size, bool parse_url = true)
109
0
{
110
0
    size_t i, j;
111
0
    char * substr_ptr;
112
0
113
0
    for(i=0; i<qs_kv_size; i++)  qs_kv[i] = NULL;
114
0
115
0
    // find the beginning of the k/v substrings or the fragment
116
0
    substr_ptr = parse_url ? qs + strcspn(qs, "?#") : qs;
117
0
    if (parse_url)
118
0
    {
119
0
        if (substr_ptr[0] != '\0')
120
0
            substr_ptr++;
121
0
        else
122
0
            return 0; // no query or fragment
123
0
    }
124
0
125
0
    i=0;
126
0
    while(i<qs_kv_size)
127
0
    {
128
0
        qs_kv[i] = substr_ptr;
129
0
        j = strcspn(substr_ptr, "&");
130
0
        if ( substr_ptr[j] == '\0' ) { i++; break;  } // x &'s -> means x iterations of this loop -> means *x+1* k/v pairs
131
0
        substr_ptr += j + 1;
132
0
        i++;
133
0
    }
134
0
135
0
    // we only decode the values in place, the keys could have '='s in them
136
0
    // which will hose our ability to distinguish keys from values later
137
0
    for(j=0; j<i; j++)
138
0
    {
139
0
        substr_ptr = qs_kv[j] + strcspn(qs_kv[j], "=&#");
140
0
        if ( substr_ptr[0] == '&' || substr_ptr[0] == '\0')  // blank value: skip decoding
141
0
            substr_ptr[0] = '\0';
142
0
        else
143
0
            qs_decode(++substr_ptr);
144
0
    }
145
0
146
0
#ifdef _qsSORTING
147
0
// TODO: qsort qs_kv, using qs_strncmp() for the comparison
148
0
#endif
149
0
150
0
    return i;
151
0
}
152
153
154
inline int qs_decode(char * qs)
155
0
{
156
0
    int i=0, j=0;
157
0
158
0
    while( CROW_QS_ISQSCHR(qs[j]) )
159
0
    {
160
0
        if ( qs[j] == '+' ) {  qs[i] = ' ';  }
161
0
        else if ( qs[j] == '%' ) // easier/safer than scanf
162
0
        {
163
0
            // Check bounds before reading: ensure j+1 and j+2 are within string
164
0
            if ( qs[j+1] == '\0' || qs[j+2] == '\0' ||
165
0
                 ! CROW_QS_ISHEX(qs[j+1]) || ! CROW_QS_ISHEX(qs[j+2]) )
166
0
            {
167
0
                qs[i] = '\0';
168
0
                return i;
169
0
            }
170
0
            qs[i] = (CROW_QS_HEX2DEC(qs[j+1]) * 16) + CROW_QS_HEX2DEC(qs[j+2]);
171
0
            j+=2;
172
0
        }
173
0
        else
174
0
        {
175
0
            qs[i] = qs[j];
176
0
        }
177
0
        i++;  j++;
178
0
    }
179
0
    qs[i] = '\0';
180
0
181
0
    return i;
182
0
}
183
184
185
inline char * qs_k2v(const char * key, char * const * qs_kv, size_t qs_kv_size, int nth = 0)
186
0
{
187
0
    size_t i;
188
0
    size_t key_len, skip;
189
0
190
0
    key_len = strlen(key);
191
0
192
0
#ifdef _qsSORTING
193
0
// TODO: binary search for key in the sorted qs_kv
194
0
#else  // _qsSORTING
195
0
    for(i=0; i<qs_kv_size; i++)
196
0
    {
197
0
        // we rely on the unambiguous '=' to find the value in our k/v pair
198
0
        if ( qs_strncmp(key, qs_kv[i], key_len) == 0 )
199
0
        {
200
0
            skip = strcspn(qs_kv[i], "=");
201
0
            if ( qs_kv[i][skip] == '=' )
202
0
                skip++;
203
0
            // return (zero-char value) ? ptr to trailing '\0' : ptr to value
204
0
            if(nth == 0)
205
0
                return qs_kv[i] + skip;
206
0
            else
207
0
                --nth;
208
0
        }
209
0
    }
210
0
#endif  // _qsSORTING
211
0
212
0
    return nullptr;
213
0
}
214
215
inline std::unique_ptr<std::pair<std::string, std::string>> qs_dict_name2kv(const char * dict_name, char * const * qs_kv, size_t qs_kv_size, int nth = 0)
216
0
{
217
0
    size_t i;
218
0
    size_t name_len, skip_to_eq, skip_to_brace_open, skip_to_brace_close;
219
0
220
0
    name_len = strlen(dict_name);
221
0
222
0
#ifdef _qsSORTING
223
0
// TODO: binary search for key in the sorted qs_kv
224
0
#else  // _qsSORTING
225
0
    for(i=0; i<qs_kv_size; i++)
226
0
    {
227
0
        if ( strncmp(dict_name, qs_kv[i], name_len) == 0 )
228
0
        {
229
0
            skip_to_eq = strcspn(qs_kv[i], "=");
230
0
            if ( qs_kv[i][skip_to_eq] == '=' )
231
0
                skip_to_eq++;
232
0
            skip_to_brace_open = strcspn(qs_kv[i], "[");
233
0
            if ( qs_kv[i][skip_to_brace_open] == '[' )
234
0
                skip_to_brace_open++;
235
0
            skip_to_brace_close = strcspn(qs_kv[i], "]");
236
0
237
0
            // Encoded brackets: page%5Bsize%5D=3 should match page[size]=3 (#1109).
238
0
            if ( skip_to_brace_open == strlen(qs_kv[i]) )
239
0
            {
240
0
                const char* open = strstr(qs_kv[i] + name_len, "%5B");
241
0
                if (!open)
242
0
                    open = strstr(qs_kv[i] + name_len, "%5b");
243
0
                if ( open && open == qs_kv[i] + name_len )
244
0
                {
245
0
                    const char* close = strstr(open + 3, "%5D");
246
0
                    if (!close)
247
0
                        close = strstr(open + 3, "%5d");
248
0
                    if ( close && nth == 0 )
249
0
                    {
250
0
                        auto key = std::string(open + 3, static_cast<size_t>(close - (open + 3)));
251
0
                        auto value = std::string(qs_kv[i] + skip_to_eq);
252
0
                        return std::unique_ptr<std::pair<std::string, std::string>>(new std::pair<std::string, std::string>(key, value));
253
0
                    }
254
0
                    else if ( close )
255
0
                    {
256
0
                        --nth;
257
0
                        continue;
258
0
                    }
259
0
                }
260
0
            }
261
0
262
0
            if ( skip_to_brace_open <= skip_to_brace_close &&
263
0
                 skip_to_brace_open > 0 &&
264
0
                 skip_to_brace_close > 0 &&
265
0
                 nth == 0 )
266
0
            {
267
0
                auto key = std::string(qs_kv[i] + skip_to_brace_open, skip_to_brace_close - skip_to_brace_open);
268
0
                auto value = std::string(qs_kv[i] + skip_to_eq);
269
0
                return std::unique_ptr<std::pair<std::string, std::string>>(new std::pair<std::string, std::string>(key, value));
270
0
            }
271
0
            else
272
0
            {
273
0
                --nth;
274
0
            }
275
0
        }
276
0
    }
277
0
#endif  // _qsSORTING
278
0
279
0
    return nullptr;
280
0
}
281
282
283
inline char * qs_scanvalue(const char * key, const char * qs, char * val, size_t val_len)
284
0
{
285
0
    const char * tmp= strchr(qs, '?');
286
0
287
0
    // find the beginning of the k/v substrings
288
0
    if ( tmp != nullptr )
289
0
        qs = tmp + 1;
290
0
291
0
    const size_t key_len = strlen(key);
292
0
    while(*qs != '#' && *qs != '\0')
293
0
    {
294
0
        if ( qs_strncmp(key, qs, key_len) == 0 )
295
0
            break;
296
0
        qs += strcspn(qs, "&");
297
0
        if (*qs=='&') qs++;
298
0
    }
299
0
300
0
    if ( qs[0] == '\0' ) return nullptr;
301
0
302
0
    qs += strcspn(qs, "=&#");
303
0
    if ( qs[0] == '=' )
304
0
    {
305
0
        qs++;
306
0
        size_t i = strcspn(qs, "&=#");
307
0
#ifdef _MSC_VER
308
0
        strncpy_s(val, val_len, qs, (val_len - 1)<(i + 1) ? (val_len - 1) : (i + 1));
309
0
#else
310
0
        strncpy(val, qs, (val_len - 1)<(i + 1) ? (val_len - 1) : (i + 1));
311
0
#endif
312
0
    qs_decode(val);
313
0
    }
314
0
    else
315
0
    {
316
0
        if ( val_len > 0 )
317
0
            val[0] = '\0';
318
0
    }
319
0
320
0
    return val;
321
0
}
322
}
323
// ----------------------------------------------------------------------------
324
325
326
namespace crow
327
{
328
    struct request;
329
    /// A class to represent any data coming after the `?` in the request URL into key-value pairs.
330
    class query_string
331
    {
332
    public:
333
        static const int MAX_KEY_VALUE_PAIRS_COUNT = 256;
334
335
        query_string() = default;
336
337
        query_string(const query_string& qs):
338
          url_(qs.url_)
339
0
        {
340
0
            for (auto p : qs.key_value_pairs_)
341
0
            {
342
0
                key_value_pairs_.push_back((char*)(p - qs.url_.c_str() + url_.c_str()));
343
0
            }
344
0
        }
345
346
        query_string& operator=(const query_string& qs)
347
0
        {
348
0
            url_ = qs.url_;
349
0
            key_value_pairs_.clear();
350
0
            for (auto p : qs.key_value_pairs_)
351
0
            {
352
0
                key_value_pairs_.push_back((char*)(p - qs.url_.c_str() + url_.c_str()));
353
0
            }
354
0
            return *this;
355
0
        }
356
357
        query_string& operator=(query_string&& qs) noexcept
358
0
        {
359
0
            key_value_pairs_ = std::move(qs.key_value_pairs_);
360
0
            char* old_data = (char*)qs.url_.c_str();
361
0
            url_ = std::move(qs.url_);
362
0
            for (auto& p : key_value_pairs_)
363
0
            {
364
0
                p += (char*)url_.c_str() - old_data;
365
0
            }
366
0
            return *this;
367
0
        }
368
369
370
        query_string(std::string params, bool url = true):
371
          url_(std::move(params))
372
0
        {
373
0
            if (url_.empty())
374
0
                return;
375
0
376
0
            key_value_pairs_.resize(MAX_KEY_VALUE_PAIRS_COUNT);
377
0
            size_t count = qs_parse(&url_[0], &key_value_pairs_[0], MAX_KEY_VALUE_PAIRS_COUNT, url);
378
0
379
0
            key_value_pairs_.resize(count);
380
0
            key_value_pairs_.shrink_to_fit();
381
0
        }
382
383
        void clear()
384
0
        {
385
0
            key_value_pairs_.clear();
386
0
            url_.clear();
387
0
        }
388
389
        friend std::ostream& operator<<(std::ostream& os, const query_string& qs)
390
0
        {
391
0
            os << "[ ";
392
0
            for (size_t i = 0; i < qs.key_value_pairs_.size(); ++i)
393
0
            {
394
0
                if (i)
395
0
                    os << ", ";
396
0
                os << qs.key_value_pairs_[i];
397
0
            }
398
0
            os << " ]";
399
0
            return os;
400
0
        }
401
402
        /// Get a value from a name, used for `?name=value`.
403
404
        ///
405
        /// Note: this method returns the value of the first occurrence of the key only, to return all occurrences, see \ref get_list().
406
        char* get(const std::string& name) const
407
0
        {
408
0
            char* ret = qs_k2v(name.c_str(), key_value_pairs_.data(), key_value_pairs_.size());
409
0
            return ret;
410
0
        }
411
412
        /// Works similar to \ref get() except it removes the item from the query string.
413
        char* pop(const std::string& name)
414
0
        {
415
0
            char* ret = get(name);
416
0
            if (ret != nullptr)
417
0
            {
418
0
                const std::string key_name = name + '=';
419
0
                for (unsigned int i = 0; i < key_value_pairs_.size(); i++)
420
0
                {
421
0
                    std::string str_item(key_value_pairs_[i]);
422
0
                    if (str_item.find(key_name)==0)
423
0
                    {
424
0
                        key_value_pairs_.erase(key_value_pairs_.begin() + i);
425
0
                        break;
426
0
                    }
427
0
                }
428
0
            }
429
0
            return ret;
430
0
        }
431
432
        /// Returns a list of values, passed as `?name[]=value1&name[]=value2&...name[]=valuen` with n being the size of the list.
433
434
        ///
435
        /// Note: Square brackets in the above example are controlled by `use_brackets` boolean (true by default). If set to false, the example becomes `?name=value1,name=value2...name=valuen`
436
        std::vector<char*> get_list(const std::string& name, bool use_brackets = true) const
437
0
        {
438
0
            std::vector<char*> ret;
439
0
            std::string plus = name + (use_brackets ? "[]" : "");
440
0
            char* element = nullptr;
441
0
442
0
            int count = 0;
443
0
            while (1)
444
0
            {
445
0
                element = qs_k2v(plus.c_str(), key_value_pairs_.data(), key_value_pairs_.size(), count++);
446
0
                if (!element)
447
0
                    break;
448
0
                ret.push_back(element);
449
0
            }
450
0
            return ret;
451
0
        }
452
453
        /// Similar to \ref get_list() but it removes the
454
        std::vector<char*> pop_list(const std::string& name, bool use_brackets = true)
455
0
        {
456
0
            std::vector<char*> ret = get_list(name, use_brackets);
457
0
            const size_t name_len = name.length();
458
0
            if (!ret.empty())
459
0
            {
460
0
                for (unsigned int i = 0; i < key_value_pairs_.size(); i++)
461
0
                {
462
0
                    std::string str_item(key_value_pairs_[i]);
463
0
                    if (str_item.find(name)==0) {
464
0
                      if (use_brackets && str_item.find("[]=",name_len)==name_len) {
465
0
                        key_value_pairs_.erase(key_value_pairs_.begin() + i--);
466
0
                      } else if (!use_brackets && str_item.find('=',name_len)==name_len ) {
467
0
                           key_value_pairs_.erase(key_value_pairs_.begin() + i--);
468
0
                       }
469
0
                    }
470
0
                }
471
0
            }
472
0
            return ret;
473
0
        }
474
475
        /// Works similar to \ref get_list() except the brackets are mandatory must not be empty.
476
477
        ///
478
        /// For example calling `get_dict(yourname)` on `?yourname[sub1]=42&yourname[sub2]=84` would give a map containing `{sub1 : 42, sub2 : 84}`.
479
        ///
480
        /// if your query string has both empty brackets and ones with a key inside, use pop_list() to get all the values without a key before running this method.
481
        std::unordered_map<std::string, std::string> get_dict(const std::string& name) const
482
0
        {
483
0
            std::unordered_map<std::string, std::string> ret;
484
0
485
0
            int count = 0;
486
0
            while (1)
487
0
            {
488
0
                if (auto element = qs_dict_name2kv(name.c_str(), key_value_pairs_.data(), key_value_pairs_.size(), count++))
489
0
                    ret.insert(*element);
490
0
                else
491
0
                    break;
492
0
            }
493
0
            return ret;
494
0
        }
495
496
        /// Works the same as \ref get_dict() but removes the values from the query string.
497
        std::unordered_map<std::string, std::string> pop_dict(const std::string& name)
498
0
        {
499
0
            const std::string name_value = name +'[';
500
0
            std::unordered_map<std::string, std::string> ret = get_dict(name);
501
0
            if (!ret.empty())
502
0
            {
503
0
                for (unsigned int i = 0; i < key_value_pairs_.size(); i++)
504
0
                {
505
0
                    std::string str_item(key_value_pairs_[i]);
506
0
                    if (str_item.find(name_value)==0)
507
0
                    {
508
0
                        key_value_pairs_.erase(key_value_pairs_.begin() + i--);
509
0
                    }
510
0
                }
511
0
            }
512
0
            return ret;
513
0
        }
514
515
        std::vector<std::string> keys() const
516
0
        {
517
0
            std::vector<std::string> keys;
518
0
            keys.reserve(key_value_pairs_.size());
519
0
520
0
            for (const char* const element : key_value_pairs_)
521
0
            {
522
0
                const char* delimiter = strchr(element, '=');
523
0
                if (delimiter)
524
0
                    keys.emplace_back(element, delimiter);
525
0
                else
526
0
                    keys.emplace_back(element);
527
0
            }
528
0
529
0
            return keys;
530
0
        }
531
532
    private:
533
        std::string url_;
534
        std::vector<char*> key_value_pairs_;
535
    };
536
537
} // namespace crow