1  
//
1  
//
2  
// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
2  
// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
3  
//
3  
//
4  
// Distributed under the Boost Software License, Version 1.0. (See accompanying
4  
// Distributed under the Boost Software License, Version 1.0. (See accompanying
5  
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
5  
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6  
//
6  
//
7  
// Official repository: https://github.com/cppalliance/http
7  
// Official repository: https://github.com/cppalliance/http
8  
//
8  
//
9  

9  

10  
#include "src/server/route_abnf.hpp"
10  
#include "src/server/route_abnf.hpp"
11  
#include <boost/url/grammar/error.hpp>
11  
#include <boost/url/grammar/error.hpp>
12  

12  

13  
namespace boost {
13  
namespace boost {
14  
namespace http {
14  
namespace http {
15  
namespace detail {
15  
namespace detail {
16  

16  

17  
namespace {
17  
namespace {
18  

18  

19  
//------------------------------------------------
19  
//------------------------------------------------
20  
// Character classification
20  
// Character classification
21  
//------------------------------------------------
21  
//------------------------------------------------
22  

22  

23  
// Special characters that have meaning in patterns
23  
// Special characters that have meaning in patterns
24  
constexpr bool
24  
constexpr bool
25  
is_special(char c) noexcept
25  
is_special(char c) noexcept
26  
{
26  
{
27  
    switch(c)
27  
    switch(c)
28  
    {
28  
    {
29  
    case '{':
29  
    case '{':
30  
    case '}':
30  
    case '}':
31  
    case '(':
31  
    case '(':
32  
    case ')':
32  
    case ')':
33  
    case '[':
33  
    case '[':
34  
    case ']':
34  
    case ']':
35  
    case '+':
35  
    case '+':
36  
    case '?':
36  
    case '?':
37  
    case '!':
37  
    case '!':
38  
    case ':':
38  
    case ':':
39  
    case '*':
39  
    case '*':
40  
    case '\\':
40  
    case '\\':
41  
        return true;
41  
        return true;
42  
    default:
42  
    default:
43  
        return false;
43  
        return false;
44  
    }
44  
    }
45  
}
45  
}
46  

46  

47  
// Reserved characters (parsed but invalid)
47  
// Reserved characters (parsed but invalid)
48  
constexpr bool
48  
constexpr bool
49  
is_reserved(char c) noexcept
49  
is_reserved(char c) noexcept
50  
{
50  
{
51  
    switch(c)
51  
    switch(c)
52  
    {
52  
    {
53  
    case '(':
53  
    case '(':
54  
    case ')':
54  
    case ')':
55  
    case '[':
55  
    case '[':
56  
    case ']':
56  
    case ']':
57  
    case '+':
57  
    case '+':
58  
    case '?':
58  
    case '?':
59  
    case '!':
59  
    case '!':
60  
        return true;
60  
        return true;
61  
    default:
61  
    default:
62  
        return false;
62  
        return false;
63  
    }
63  
    }
64  
}
64  
}
65  

65  

66  
// Valid identifier start (ASCII subset of ID_Start)
66  
// Valid identifier start (ASCII subset of ID_Start)
67  
constexpr bool
67  
constexpr bool
68  
is_id_start(char c) noexcept
68  
is_id_start(char c) noexcept
69  
{
69  
{
70  
    return
70  
    return
71  
        (c >= 'a' && c <= 'z') ||
71  
        (c >= 'a' && c <= 'z') ||
72  
        (c >= 'A' && c <= 'Z') ||
72  
        (c >= 'A' && c <= 'Z') ||
73  
        c == '_' || c == '$';
73  
        c == '_' || c == '$';
74  
}
74  
}
75  

75  

76  
// Valid identifier continuation (ASCII subset of ID_Continue)
76  
// Valid identifier continuation (ASCII subset of ID_Continue)
77  
constexpr bool
77  
constexpr bool
78  
is_id_continue(char c) noexcept
78  
is_id_continue(char c) noexcept
79  
{
79  
{
80  
    return
80  
    return
81  
        is_id_start(c) ||
81  
        is_id_start(c) ||
82  
        (c >= '0' && c <= '9');
82  
        (c >= '0' && c <= '9');
83  
}
83  
}
84  

84  

85  
//------------------------------------------------
85  
//------------------------------------------------
86  
// Parser state
86  
// Parser state
87  
//------------------------------------------------
87  
//------------------------------------------------
88  

88  

89  
class parser
89  
class parser
90  
{
90  
{
91  
    char const* it_;
91  
    char const* it_;
92  
    char const* end_;
92  
    char const* end_;
93  
    core::string_view original_;
93  
    core::string_view original_;
94  

94  

95  
public:
95  
public:
96  
    parser(core::string_view s)
96  
    parser(core::string_view s)
97  
        : it_(s.data())
97  
        : it_(s.data())
98  
        , end_(s.data() + s.size())
98  
        , end_(s.data() + s.size())
99  
        , original_(s)
99  
        , original_(s)
100  
    {
100  
    {
101  
    }
101  
    }
102  

102  

103  
    bool
103  
    bool
104  
    at_end() const noexcept
104  
    at_end() const noexcept
105  
    {
105  
    {
106  
        return it_ == end_;
106  
        return it_ == end_;
107  
    }
107  
    }
108  

108  

109  
    char
109  
    char
110  
    peek() const noexcept
110  
    peek() const noexcept
111  
    {
111  
    {
112  
        return *it_;
112  
        return *it_;
113  
    }
113  
    }
114  

114  

115  
    void
115  
    void
116  
    advance() noexcept
116  
    advance() noexcept
117  
    {
117  
    {
118  
        ++it_;
118  
        ++it_;
119  
    }
119  
    }
120  

120  

121  
    char
121  
    char
122  
    get() noexcept
122  
    get() noexcept
123  
    {
123  
    {
124  
        return *it_++;
124  
        return *it_++;
125  
    }
125  
    }
126  

126  

127  
    std::size_t
127  
    std::size_t
128  
    pos() const noexcept
128  
    pos() const noexcept
129  
    {
129  
    {
130  
        return static_cast<std::size_t>(
130  
        return static_cast<std::size_t>(
131  
            it_ - original_.data());
131  
            it_ - original_.data());
132  
    }
132  
    }
133  

133  

134  
    //--------------------------------------------
134  
    //--------------------------------------------
135  
    // Name parsing
135  
    // Name parsing
136  
    //--------------------------------------------
136  
    //--------------------------------------------
137  

137  

138  
    // Parse identifier: id-start *id-continue
138  
    // Parse identifier: id-start *id-continue
139  
    system::result<std::string>
139  
    system::result<std::string>
140  
    parse_identifier()
140  
    parse_identifier()
141  
    {
141  
    {
142  
        if(at_end() || !is_id_start(peek()))
142  
        if(at_end() || !is_id_start(peek()))
143  
            return grammar::error::mismatch;
143  
            return grammar::error::mismatch;
144  

144  

145  
        std::string result;
145  
        std::string result;
146  
        result += get();
146  
        result += get();
147  

147  

148  
        while(!at_end() && is_id_continue(peek()))
148  
        while(!at_end() && is_id_continue(peek()))
149  
            result += get();
149  
            result += get();
150  

150  

151  
        return result;
151  
        return result;
152  
    }
152  
    }
153  

153  

154  
    // Parse quoted name: DQUOTE *quoted-char DQUOTE
154  
    // Parse quoted name: DQUOTE *quoted-char DQUOTE
155  
    system::result<std::string>
155  
    system::result<std::string>
156  
    parse_quoted_name()
156  
    parse_quoted_name()
157  
    {
157  
    {
158  
        if(at_end() || peek() != '"')
158  
        if(at_end() || peek() != '"')
159  
            return grammar::error::mismatch;
159  
            return grammar::error::mismatch;
160  

160  

161  
        advance(); // skip opening quote
161  
        advance(); // skip opening quote
162  
        std::string result;
162  
        std::string result;
163  

163  

164  
        while(!at_end())
164  
        while(!at_end())
165  
        {
165  
        {
166  
            char c = peek();
166  
            char c = peek();
167  

167  

168  
            if(c == '"')
168  
            if(c == '"')
169  
            {
169  
            {
170  
                advance(); // skip closing quote
170  
                advance(); // skip closing quote
171  
                if(result.empty())
171  
                if(result.empty())
172  
                    return grammar::error::syntax;
172  
                    return grammar::error::syntax;
173  
                return result;
173  
                return result;
174  
            }
174  
            }
175  

175  

176  
            if(c == '\\')
176  
            if(c == '\\')
177  
            {
177  
            {
178  
                advance(); // skip backslash
178  
                advance(); // skip backslash
179  
                if(at_end())
179  
                if(at_end())
180  
                    return grammar::error::syntax;
180  
                    return grammar::error::syntax;
181  
                result += get();
181  
                result += get();
182  
            }
182  
            }
183  
            else
183  
            else
184  
            {
184  
            {
185  
                result += get();
185  
                result += get();
186  
            }
186  
            }
187  
        }
187  
        }
188  

188  

189  
        // Unterminated quote
189  
        // Unterminated quote
190  
        return grammar::error::syntax;
190  
        return grammar::error::syntax;
191  
    }
191  
    }
192  

192  

193  
    // Parse name: identifier / quoted-name
193  
    // Parse name: identifier / quoted-name
194  
    system::result<std::string>
194  
    system::result<std::string>
195  
    parse_name()
195  
    parse_name()
196  
    {
196  
    {
197  
        if(at_end())
197  
        if(at_end())
198  
            return grammar::error::syntax;
198  
            return grammar::error::syntax;
199  

199  

200  
        if(peek() == '"')
200  
        if(peek() == '"')
201  
            return parse_quoted_name();
201  
            return parse_quoted_name();
202  

202  

203  
        return parse_identifier();
203  
        return parse_identifier();
204  
    }
204  
    }
205  

205  

206  
    //--------------------------------------------
206  
    //--------------------------------------------
207  
    // Token parsing
207  
    // Token parsing
208  
    //--------------------------------------------
208  
    //--------------------------------------------
209  

209  

210  
    // Parse text: 1*(char / escaped-char)
210  
    // Parse text: 1*(char / escaped-char)
211  
    system::result<route_token>
211  
    system::result<route_token>
212  
    parse_text()
212  
    parse_text()
213  
    {
213  
    {
214  
        std::string result;
214  
        std::string result;
215  

215  

216  
        while(!at_end())
216  
        while(!at_end())
217  
        {
217  
        {
218  
            char c = peek();
218  
            char c = peek();
219  

219  

220  
            // Stop at special characters
220  
            // Stop at special characters
221  
            if(is_special(c))
221  
            if(is_special(c))
222  
            {
222  
            {
223  
                if(c == '\\')
223  
                if(c == '\\')
224  
                {
224  
                {
225  
                    // Escaped character
225  
                    // Escaped character
226  
                    advance();
226  
                    advance();
227  
                    if(at_end())
227  
                    if(at_end())
228  
                        return grammar::error::syntax;
228  
                        return grammar::error::syntax;
229  
                    result += get();
229  
                    result += get();
230  
                    continue;
230  
                    continue;
231  
                }
231  
                }
232  
                break;
232  
                break;
233  
            }
233  
            }
234  

234  

235  
            result += get();
235  
            result += get();
236  
        }
236  
        }
237  

237  

238  
        if(result.empty())
238  
        if(result.empty())
239  
            return grammar::error::mismatch;
239  
            return grammar::error::mismatch;
240  

240  

241  
        return route_token(route_token_type::text, std::move(result));
241  
        return route_token(route_token_type::text, std::move(result));
242  
    }
242  
    }
243  

243  

244  
    // Parse param: ":" name
244  
    // Parse param: ":" name
245  
    system::result<route_token>
245  
    system::result<route_token>
246  
    parse_param()
246  
    parse_param()
247  
    {
247  
    {
248  
        if(at_end() || peek() != ':')
248  
        if(at_end() || peek() != ':')
249  
            return grammar::error::mismatch;
249  
            return grammar::error::mismatch;
250  

250  

251  
        advance(); // skip ':'
251  
        advance(); // skip ':'
252  

252  

253  
        auto rv = parse_name();
253  
        auto rv = parse_name();
254  
        if(rv.has_error())
254  
        if(rv.has_error())
255  
            return rv.error();
255  
            return rv.error();
256  

256  

257  
        return route_token(
257  
        return route_token(
258  
            route_token_type::param, std::move(rv.value()));
258  
            route_token_type::param, std::move(rv.value()));
259  
    }
259  
    }
260  

260  

261  
    // Parse wildcard: "*" name
261  
    // Parse wildcard: "*" name
262  
    system::result<route_token>
262  
    system::result<route_token>
263  
    parse_wildcard()
263  
    parse_wildcard()
264  
    {
264  
    {
265  
        if(at_end() || peek() != '*')
265  
        if(at_end() || peek() != '*')
266  
            return grammar::error::mismatch;
266  
            return grammar::error::mismatch;
267  

267  

268  
        advance(); // skip '*'
268  
        advance(); // skip '*'
269  

269  

270  
        auto rv = parse_name();
270  
        auto rv = parse_name();
271  
        if(rv.has_error())
271  
        if(rv.has_error())
272  
            return rv.error();
272  
            return rv.error();
273  

273  

274  
        return route_token(
274  
        return route_token(
275  
            route_token_type::wildcard, std::move(rv.value()));
275  
            route_token_type::wildcard, std::move(rv.value()));
276  
    }
276  
    }
277  

277  

278  
    // Parse group: "{" *token "}"
278  
    // Parse group: "{" *token "}"
279  
    system::result<route_token>
279  
    system::result<route_token>
280  
    parse_group()
280  
    parse_group()
281  
    {
281  
    {
282  
        if(at_end() || peek() != '{')
282  
        if(at_end() || peek() != '{')
283  
            return grammar::error::mismatch;
283  
            return grammar::error::mismatch;
284  

284  

285  
        advance(); // skip '{'
285  
        advance(); // skip '{'
286  

286  

287  
        route_token group;
287  
        route_token group;
288  
        group.type = route_token_type::group;
288  
        group.type = route_token_type::group;
289  

289  

290  
        // Parse tokens until '}'
290  
        // Parse tokens until '}'
291  
        while(!at_end() && peek() != '}')
291  
        while(!at_end() && peek() != '}')
292  
        {
292  
        {
293  
            auto rv = parse_token();
293  
            auto rv = parse_token();
294  
            if(rv.has_error())
294  
            if(rv.has_error())
295  
                return rv.error();
295  
                return rv.error();
296  
            group.children.push_back(std::move(rv.value()));
296  
            group.children.push_back(std::move(rv.value()));
297  
        }
297  
        }
298  

298  

299  
        if(at_end())
299  
        if(at_end())
300  
            return grammar::error::syntax; // unclosed group
300  
            return grammar::error::syntax; // unclosed group
301  

301  

302  
        advance(); // skip '}'
302  
        advance(); // skip '}'
303  

303  

304  
        return group;
304  
        return group;
305  
    }
305  
    }
306  

306  

307  
    // Parse single token
307  
    // Parse single token
308  
    system::result<route_token>
308  
    system::result<route_token>
309  
    parse_token()
309  
    parse_token()
310  
    {
310  
    {
311  
        if(at_end())
311  
        if(at_end())
312  
            return grammar::error::syntax;
312  
            return grammar::error::syntax;
313  

313  

314  
        char c = peek();
314  
        char c = peek();
315  

315  

316  
        // Check for reserved characters
316  
        // Check for reserved characters
317  
        if(is_reserved(c))
317  
        if(is_reserved(c))
318  
            return grammar::error::syntax;
318  
            return grammar::error::syntax;
319  

319  

320  
        // Try each token type
320  
        // Try each token type
321  
        if(c == ':')
321  
        if(c == ':')
322  
            return parse_param();
322  
            return parse_param();
323  

323  

324  
        if(c == '*')
324  
        if(c == '*')
325  
            return parse_wildcard();
325  
            return parse_wildcard();
326  

326  

327  
        if(c == '{')
327  
        if(c == '{')
328  
            return parse_group();
328  
            return parse_group();
329  

329  

330  
        if(c == '}')
330  
        if(c == '}')
331  
            return grammar::error::syntax; // unexpected '}'
331  
            return grammar::error::syntax; // unexpected '}'
332  

332  

333  
        // Must be text
333  
        // Must be text
334  
        return parse_text();
334  
        return parse_text();
335  
    }
335  
    }
336  

336  

337  
    // Parse entire pattern
337  
    // Parse entire pattern
338  
    system::result<std::vector<route_token>>
338  
    system::result<std::vector<route_token>>
339  
    parse_tokens()
339  
    parse_tokens()
340  
    {
340  
    {
341  
        std::vector<route_token> tokens;
341  
        std::vector<route_token> tokens;
342  

342  

343  
        while(!at_end())
343  
        while(!at_end())
344  
        {
344  
        {
345  
            auto rv = parse_token();
345  
            auto rv = parse_token();
346  
            if(rv.has_error())
346  
            if(rv.has_error())
347  
                return rv.error();
347  
                return rv.error();
348  
            tokens.push_back(std::move(rv.value()));
348  
            tokens.push_back(std::move(rv.value()));
349  
        }
349  
        }
350  

350  

351  
        return tokens;
351  
        return tokens;
352  
    }
352  
    }
353  
};
353  
};
354  

354  

355  
//------------------------------------------------
355  
//------------------------------------------------
356  
// Case-insensitive comparison
356  
// Case-insensitive comparison
357  
//------------------------------------------------
357  
//------------------------------------------------
358  

358  

359  
bool
359  
bool
360  
ci_equal(char a, char b) noexcept
360  
ci_equal(char a, char b) noexcept
361  
{
361  
{
362  
    if(a >= 'A' && a <= 'Z')
362  
    if(a >= 'A' && a <= 'Z')
363  
        a = static_cast<char>(a + 32);
363  
        a = static_cast<char>(a + 32);
364  
    if(b >= 'A' && b <= 'Z')
364  
    if(b >= 'A' && b <= 'Z')
365  
        b = static_cast<char>(b + 32);
365  
        b = static_cast<char>(b + 32);
366  
    return a == b;
366  
    return a == b;
367  
}
367  
}
368  

368  

369  
bool
369  
bool
370  
ci_starts_with(
370  
ci_starts_with(
371  
    core::string_view str,
371  
    core::string_view str,
372  
    core::string_view prefix) noexcept
372  
    core::string_view prefix) noexcept
373  
{
373  
{
374  
    if(prefix.size() > str.size())
374  
    if(prefix.size() > str.size())
375  
        return false;
375  
        return false;
376  
    for(std::size_t i = 0; i < prefix.size(); ++i)
376  
    for(std::size_t i = 0; i < prefix.size(); ++i)
377  
    {
377  
    {
378  
        if(!ci_equal(str[i], prefix[i]))
378  
        if(!ci_equal(str[i], prefix[i]))
379  
            return false;
379  
            return false;
380  
    }
380  
    }
381  
    return true;
381  
    return true;
382  
}
382  
}
383  

383  

384  
//------------------------------------------------
384  
//------------------------------------------------
385  
// Route matcher
385  
// Route matcher
386  
//------------------------------------------------
386  
//------------------------------------------------
387  

387  

388  
class route_matcher
388  
class route_matcher
389  
{
389  
{
390  
    core::string_view path_;
390  
    core::string_view path_;
391  
    match_options const& opts_;
391  
    match_options const& opts_;
392  
    std::vector<std::pair<std::string, std::string>> params_;
392  
    std::vector<std::pair<std::string, std::string>> params_;
393  
    std::size_t pos_ = 0;
393  
    std::size_t pos_ = 0;
394  

394  

395  
public:
395  
public:
396  
    route_matcher(
396  
    route_matcher(
397  
        core::string_view path,
397  
        core::string_view path,
398  
        match_options const& opts)
398  
        match_options const& opts)
399  
        : path_(path)
399  
        : path_(path)
400  
        , opts_(opts)
400  
        , opts_(opts)
401  
    {
401  
    {
402  
    }
402  
    }
403  

403  

404  
    bool at_end() const noexcept
404  
    bool at_end() const noexcept
405  
    {
405  
    {
406  
        return pos_ >= path_.size();
406  
        return pos_ >= path_.size();
407  
    }
407  
    }
408  

408  

409  
    std::size_t pos() const noexcept
409  
    std::size_t pos() const noexcept
410  
    {
410  
    {
411  
        return pos_;
411  
        return pos_;
412  
    }
412  
    }
413  

413  

414  
    std::vector<std::pair<std::string, std::string>> const&
414  
    std::vector<std::pair<std::string, std::string>> const&
415  
    params() const noexcept
415  
    params() const noexcept
416  
    {
416  
    {
417  
        return params_;
417  
        return params_;
418  
    }
418  
    }
419  

419  

420  
    // Match text token
420  
    // Match text token
421  
    bool match_text(core::string_view text)
421  
    bool match_text(core::string_view text)
422  
    {
422  
    {
423  
        auto remaining = path_.substr(pos_);
423  
        auto remaining = path_.substr(pos_);
424  
        if(opts_.case_sensitive)
424  
        if(opts_.case_sensitive)
425  
        {
425  
        {
426  
            if(!remaining.starts_with(text))
426  
            if(!remaining.starts_with(text))
427  
                return false;
427  
                return false;
428  
        }
428  
        }
429  
        else
429  
        else
430  
        {
430  
        {
431  
            if(!ci_starts_with(remaining, text))
431  
            if(!ci_starts_with(remaining, text))
432  
                return false;
432  
                return false;
433  
        }
433  
        }
434  
        pos_ += text.size();
434  
        pos_ += text.size();
435  
        return true;
435  
        return true;
436  
    }
436  
    }
437  

437  

438  
    // Match param token - capture until stop_char, '/' or end
438  
    // Match param token - capture until stop_char, '/' or end
439  
    bool match_param(std::string const& name, char stop_char = '\0')
439  
    bool match_param(std::string const& name, char stop_char = '\0')
440  
    {
440  
    {
441  
        if(at_end())
441  
        if(at_end())
442  
            return false;
442  
            return false;
443  

443  

444  
        auto start = pos_;
444  
        auto start = pos_;
445  
        while(pos_ < path_.size() && path_[pos_] != '/')
445  
        while(pos_ < path_.size() && path_[pos_] != '/')
446  
        {
446  
        {
447  
            // Stop at delimiter if specified
447  
            // Stop at delimiter if specified
448  
            if(stop_char != '\0' && path_[pos_] == stop_char)
448  
            if(stop_char != '\0' && path_[pos_] == stop_char)
449  
                break;
449  
                break;
450  
            ++pos_;
450  
            ++pos_;
451  
        }
451  
        }
452  

452  

453  
        // Param must capture at least one character
453  
        // Param must capture at least one character
454  
        if(pos_ == start)
454  
        if(pos_ == start)
455  
            return false;
455  
            return false;
456  

456  

457  
        params_.emplace_back(
457  
        params_.emplace_back(
458  
            name,
458  
            name,
459  
            std::string(path_.substr(start, pos_ - start)));
459  
            std::string(path_.substr(start, pos_ - start)));
460  
        return true;
460  
        return true;
461  
    }
461  
    }
462  

462  

463  
    // Match wildcard token - capture everything to end
463  
    // Match wildcard token - capture everything to end
464  
    bool match_wildcard(std::string const& name)
464  
    bool match_wildcard(std::string const& name)
465  
    {
465  
    {
466  
        if(at_end())
466  
        if(at_end())
467  
            return false;
467  
            return false;
468  

468  

469  
        auto start = pos_;
469  
        auto start = pos_;
470  
        pos_ = path_.size();
470  
        pos_ = path_.size();
471  

471  

472  
        // Wildcard must capture at least one character
472  
        // Wildcard must capture at least one character
473  
        if(pos_ == start)
473  
        if(pos_ == start)
474  
            return false;
474  
            return false;
475  

475  

476  
        params_.emplace_back(
476  
        params_.emplace_back(
477  
            name,
477  
            name,
478  
            std::string(path_.substr(start)));
478  
            std::string(path_.substr(start)));
479  
        return true;
479  
        return true;
480  
    }
480  
    }
481  

481  

482  
    // Get the first character of the next meaningful token
482  
    // Get the first character of the next meaningful token
483  
    // Returns '\0' if none exists or next token is not text
483  
    // Returns '\0' if none exists or next token is not text
484  
    static char
484  
    static char
485  
    get_stop_char(
485  
    get_stop_char(
486  
        std::vector<route_token> const& tokens,
486  
        std::vector<route_token> const& tokens,
487  
        std::size_t next_idx)
487  
        std::size_t next_idx)
488  
    {
488  
    {
489  
        if(next_idx >= tokens.size())
489  
        if(next_idx >= tokens.size())
490  
            return '\0';
490  
            return '\0';
491  

491  

492  
        auto const& next = tokens[next_idx];
492  
        auto const& next = tokens[next_idx];
493  
        if(next.type == route_token_type::text && !next.value.empty())
493  
        if(next.type == route_token_type::text && !next.value.empty())
494  
            return next.value[0];
494  
            return next.value[0];
495  

495  

496  
        return '\0';
496  
        return '\0';
497  
    }
497  
    }
498  

498  

499  
    // Match a sequence of tokens
499  
    // Match a sequence of tokens
500  
    bool match_tokens(std::vector<route_token> const& tokens)
500  
    bool match_tokens(std::vector<route_token> const& tokens)
501  
    {
501  
    {
502  
        for(std::size_t i = 0; i < tokens.size(); ++i)
502  
        for(std::size_t i = 0; i < tokens.size(); ++i)
503  
        {
503  
        {
504  
            if(!match_token(tokens[i], get_stop_char(tokens, i + 1)))
504  
            if(!match_token(tokens[i], get_stop_char(tokens, i + 1)))
505  
                return false;
505  
                return false;
506  
        }
506  
        }
507  
        return true;
507  
        return true;
508  
    }
508  
    }
509  

509  

510  
    // Match a single token
510  
    // Match a single token
511  
    bool match_token(route_token const& token, char stop_char = '\0')
511  
    bool match_token(route_token const& token, char stop_char = '\0')
512  
    {
512  
    {
513  
        switch(token.type)
513  
        switch(token.type)
514  
        {
514  
        {
515  
        case route_token_type::text:
515  
        case route_token_type::text:
516  
            return match_text(token.value);
516  
            return match_text(token.value);
517  

517  

518  
        case route_token_type::param:
518  
        case route_token_type::param:
519  
            return match_param(token.value, stop_char);
519  
            return match_param(token.value, stop_char);
520  

520  

521  
        case route_token_type::wildcard:
521  
        case route_token_type::wildcard:
522  
            return match_wildcard(token.value);
522  
            return match_wildcard(token.value);
523  

523  

524  
        case route_token_type::group:
524  
        case route_token_type::group:
525  
            return match_group(token.children);
525  
            return match_group(token.children);
526  

526  

527  
        default:
527  
        default:
528  
            return false;
528  
            return false;
529  
        }
529  
        }
530  
    }
530  
    }
531  

531  

532  
    // Match group - try with contents, then without
532  
    // Match group - try with contents, then without
533  
    bool match_group(std::vector<route_token> const& children)
533  
    bool match_group(std::vector<route_token> const& children)
534  
    {
534  
    {
535  
        // Save state before trying group
535  
        // Save state before trying group
536  
        auto saved_pos = pos_;
536  
        auto saved_pos = pos_;
537  
        auto saved_params_size = params_.size();
537  
        auto saved_params_size = params_.size();
538  

538  

539  
        // Try matching with group contents
539  
        // Try matching with group contents
540  
        if(match_tokens(children))
540  
        if(match_tokens(children))
541  
            return true;
541  
            return true;
542  

542  

543  
        // Restore state and try without group
543  
        // Restore state and try without group
544  
        pos_ = saved_pos;
544  
        pos_ = saved_pos;
545  
        params_.resize(saved_params_size);
545  
        params_.resize(saved_params_size);
546  
        return true;  // Group is optional, always succeeds if skipped
546  
        return true;  // Group is optional, always succeeds if skipped
547  
    }
547  
    }
548  

548  

549  
    // Check if match is complete based on options
549  
    // Check if match is complete based on options
550  
    bool is_complete() const
550  
    bool is_complete() const
551  
    {
551  
    {
552  
        if(!opts_.end)
552  
        if(!opts_.end)
553  
            return true;  // Prefix match always succeeds
553  
            return true;  // Prefix match always succeeds
554  

554  

555  
        if(opts_.strict)
555  
        if(opts_.strict)
556  
            return at_end();
556  
            return at_end();
557  

557  

558  
        // Non-strict: allow trailing slash
558  
        // Non-strict: allow trailing slash
559  
        if(at_end())
559  
        if(at_end())
560  
            return true;
560  
            return true;
561  
        if(pos_ == path_.size() - 1 && path_[pos_] == '/')
561  
        if(pos_ == path_.size() - 1 && path_[pos_] == '/')
562  
            return true;
562  
            return true;
563  

563  

564  
        return false;
564  
        return false;
565  
    }
565  
    }
566  
};
566  
};
567  

567  

568  
} // anonymous namespace
568  
} // anonymous namespace
569  

569  

570  
//------------------------------------------------
570  
//------------------------------------------------
571  

571  

572  
system::result<route_pattern>
572  
system::result<route_pattern>
573  
parse_route_pattern(core::string_view pattern)
573  
parse_route_pattern(core::string_view pattern)
574  
{
574  
{
575  
    parser p(pattern);
575  
    parser p(pattern);
576  
    auto rv = p.parse_tokens();
576  
    auto rv = p.parse_tokens();
577  
    if(rv.has_error())
577  
    if(rv.has_error())
578  
        return rv.error();
578  
        return rv.error();
579  

579  

580  
    route_pattern result;
580  
    route_pattern result;
581  
    result.tokens = std::move(rv.value());
581  
    result.tokens = std::move(rv.value());
582  
    result.original = std::string(pattern);
582  
    result.original = std::string(pattern);
583  
    return result;
583  
    return result;
584  
}
584  
}
585  

585  

586  
//------------------------------------------------
586  
//------------------------------------------------
587  

587  

588  
system::result<match_params>
588  
system::result<match_params>
589  
match_route(
589  
match_route(
590  
    core::string_view path,
590  
    core::string_view path,
591  
    route_pattern const& pattern,
591  
    route_pattern const& pattern,
592  
    match_options const& opts)
592  
    match_options const& opts)
593  
{
593  
{
594  
    route_matcher m(path, opts);
594  
    route_matcher m(path, opts);
595  

595  

596  
    if(!m.match_tokens(pattern.tokens))
596  
    if(!m.match_tokens(pattern.tokens))
597  
        return grammar::error::mismatch;
597  
        return grammar::error::mismatch;
598  

598  

599  
    if(!m.is_complete())
599  
    if(!m.is_complete())
600  
        return grammar::error::mismatch;
600  
        return grammar::error::mismatch;
601  

601  

602  
    match_params result;
602  
    match_params result;
603  
    result.params = m.params();
603  
    result.params = m.params();
604  
    result.matched_length = m.pos();
604  
    result.matched_length = m.pos();
605  
    return result;
605  
    return result;
606  
}
606  
}
607  

607  

608  
} // detail
608  
} // detail
609  
} // http
609  
} // http
610  
} // boost
610  
} // boost