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 <boost/http/server/range_parser.hpp>
10  
#include <boost/http/server/range_parser.hpp>
11  
#include <algorithm>
11  
#include <algorithm>
12  
#include <cctype>
12  
#include <cctype>
13  
#include <charconv>
13  
#include <charconv>
14  

14  

15  
namespace boost {
15  
namespace boost {
16  
namespace http {
16  
namespace http {
17  

17  

18  
namespace {
18  
namespace {
19  

19  

20  
// Skip whitespace
20  
// Skip whitespace
21  
void
21  
void
22  
skip_ws( core::string_view& s ) noexcept
22  
skip_ws( core::string_view& s ) noexcept
23  
{
23  
{
24  
    while( ! s.empty() && std::isspace(
24  
    while( ! s.empty() && std::isspace(
25  
        static_cast<unsigned char>( s.front() ) ) )
25  
        static_cast<unsigned char>( s.front() ) ) )
26  
        s.remove_prefix( 1 );
26  
        s.remove_prefix( 1 );
27  
}
27  
}
28  

28  

29  
// Parse integer
29  
// Parse integer
30  
bool
30  
bool
31  
parse_int( core::string_view& s, std::int64_t& out ) noexcept
31  
parse_int( core::string_view& s, std::int64_t& out ) noexcept
32  
{
32  
{
33  
    skip_ws( s );
33  
    skip_ws( s );
34  
    if( s.empty() )
34  
    if( s.empty() )
35  
        return false;
35  
        return false;
36  

36  

37  
    auto const* begin = s.data();
37  
    auto const* begin = s.data();
38  
    auto const* end = s.data() + s.size();
38  
    auto const* end = s.data() + s.size();
39  
    auto [ptr, ec] = std::from_chars( begin, end, out );
39  
    auto [ptr, ec] = std::from_chars( begin, end, out );
40  
    if( ec != std::errc() || ptr == begin )
40  
    if( ec != std::errc() || ptr == begin )
41  
        return false;
41  
        return false;
42  

42  

43  
    s.remove_prefix( static_cast<std::size_t>( ptr - begin ) );
43  
    s.remove_prefix( static_cast<std::size_t>( ptr - begin ) );
44  
    return true;
44  
    return true;
45  
}
45  
}
46  

46  

47  
// Parse a single range spec: "start-end" or "-suffix" or "start-"
47  
// Parse a single range spec: "start-end" or "-suffix" or "start-"
48  
bool
48  
bool
49  
parse_range_spec(
49  
parse_range_spec(
50  
    core::string_view& s,
50  
    core::string_view& s,
51  
    std::int64_t size,
51  
    std::int64_t size,
52  
    byte_range& out )
52  
    byte_range& out )
53  
{
53  
{
54  
    skip_ws( s );
54  
    skip_ws( s );
55  
    if( s.empty() )
55  
    if( s.empty() )
56  
        return false;
56  
        return false;
57  

57  

58  
    std::int64_t start = -1;
58  
    std::int64_t start = -1;
59  
    std::int64_t end = -1;
59  
    std::int64_t end = -1;
60  

60  

61  
    // Check for suffix range: "-suffix"
61  
    // Check for suffix range: "-suffix"
62  
    if( s.front() == '-' )
62  
    if( s.front() == '-' )
63  
    {
63  
    {
64  
        s.remove_prefix( 1 );
64  
        s.remove_prefix( 1 );
65  
        std::int64_t suffix;
65  
        std::int64_t suffix;
66  
        if( ! parse_int( s, suffix ) || suffix < 0 )
66  
        if( ! parse_int( s, suffix ) || suffix < 0 )
67  
            return false;
67  
            return false;
68  

68  

69  
        // Last 'suffix' bytes
69  
        // Last 'suffix' bytes
70  
        if( suffix == 0 )
70  
        if( suffix == 0 )
71  
            return false;
71  
            return false;
72  

72  

73  
        if( suffix > size )
73  
        if( suffix > size )
74  
            suffix = size;
74  
            suffix = size;
75  

75  

76  
        out.start = size - suffix;
76  
        out.start = size - suffix;
77  
        out.end = size - 1;
77  
        out.end = size - 1;
78  
        return true;
78  
        return true;
79  
    }
79  
    }
80  

80  

81  
    // Parse start
81  
    // Parse start
82  
    if( ! parse_int( s, start ) || start < 0 )
82  
    if( ! parse_int( s, start ) || start < 0 )
83  
        return false;
83  
        return false;
84  

84  

85  
    skip_ws( s );
85  
    skip_ws( s );
86  
    if( s.empty() || s.front() != '-' )
86  
    if( s.empty() || s.front() != '-' )
87  
        return false;
87  
        return false;
88  

88  

89  
    s.remove_prefix( 1 ); // consume '-'
89  
    s.remove_prefix( 1 ); // consume '-'
90  

90  

91  
    // Check for "start-" (open-ended)
91  
    // Check for "start-" (open-ended)
92  
    skip_ws( s );
92  
    skip_ws( s );
93  
    if( s.empty() || s.front() == ',' )
93  
    if( s.empty() || s.front() == ',' )
94  
    {
94  
    {
95  
        // Open-ended: start to end of file
95  
        // Open-ended: start to end of file
96  
        out.start = start;
96  
        out.start = start;
97  
        out.end = size - 1;
97  
        out.end = size - 1;
98  
        return start < size;
98  
        return start < size;
99  
    }
99  
    }
100  

100  

101  
    // Parse end
101  
    // Parse end
102  
    if( ! parse_int( s, end ) || end < 0 )
102  
    if( ! parse_int( s, end ) || end < 0 )
103  
        return false;
103  
        return false;
104  

104  

105  
    // Validate
105  
    // Validate
106  
    if( start > end )
106  
    if( start > end )
107  
        return false;
107  
        return false;
108  

108  

109  
    out.start = start;
109  
    out.start = start;
110  
    out.end = ( std::min )( end, size - 1 );
110  
    out.end = ( std::min )( end, size - 1 );
111  

111  

112  
    return start < size;
112  
    return start < size;
113  
}
113  
}
114  

114  

115  
} // (anon)
115  
} // (anon)
116  

116  

117  
range_result
117  
range_result
118  
parse_range( std::int64_t size, core::string_view header )
118  
parse_range( std::int64_t size, core::string_view header )
119  
{
119  
{
120  
    range_result result;
120  
    range_result result;
121  
    result.type = range_result_type::malformed;
121  
    result.type = range_result_type::malformed;
122  

122  

123  
    if( size <= 0 )
123  
    if( size <= 0 )
124  
    {
124  
    {
125  
        result.type = range_result_type::unsatisfiable;
125  
        result.type = range_result_type::unsatisfiable;
126  
        return result;
126  
        return result;
127  
    }
127  
    }
128  

128  

129  
    // Must start with "bytes="
129  
    // Must start with "bytes="
130  
    skip_ws( header );
130  
    skip_ws( header );
131  
    if( header.size() < 6 )
131  
    if( header.size() < 6 )
132  
        return result;
132  
        return result;
133  

133  

134  
    // Case-insensitive "bytes=" check
134  
    // Case-insensitive "bytes=" check
135  
    auto prefix = header.substr( 0, 6 );
135  
    auto prefix = header.substr( 0, 6 );
136  
    bool is_bytes = true;
136  
    bool is_bytes = true;
137  
    for( std::size_t i = 0; i < 5; ++i )
137  
    for( std::size_t i = 0; i < 5; ++i )
138  
    {
138  
    {
139  
        char c = static_cast<char>( std::tolower(
139  
        char c = static_cast<char>( std::tolower(
140  
            static_cast<unsigned char>( prefix[i] ) ) );
140  
            static_cast<unsigned char>( prefix[i] ) ) );
141  
        if( c != "bytes"[i] )
141  
        if( c != "bytes"[i] )
142  
        {
142  
        {
143  
            is_bytes = false;
143  
            is_bytes = false;
144  
            break;
144  
            break;
145  
        }
145  
        }
146  
    }
146  
    }
147  
    if( ! is_bytes || prefix[5] != '=' )
147  
    if( ! is_bytes || prefix[5] != '=' )
148  
        return result;
148  
        return result;
149  

149  

150  
    header.remove_prefix( 6 );
150  
    header.remove_prefix( 6 );
151  

151  

152  
    // Parse range specs
152  
    // Parse range specs
153  
    bool any_satisfiable = false;
153  
    bool any_satisfiable = false;
154  

154  

155  
    while( ! header.empty() )
155  
    while( ! header.empty() )
156  
    {
156  
    {
157  
        skip_ws( header );
157  
        skip_ws( header );
158  
        if( header.empty() )
158  
        if( header.empty() )
159  
            break;
159  
            break;
160  

160  

161  
        byte_range range;
161  
        byte_range range;
162  
        if( parse_range_spec( header, size, range ) )
162  
        if( parse_range_spec( header, size, range ) )
163  
        {
163  
        {
164  
            result.ranges.push_back( range );
164  
            result.ranges.push_back( range );
165  
            any_satisfiable = true;
165  
            any_satisfiable = true;
166  
        }
166  
        }
167  

167  

168  
        skip_ws( header );
168  
        skip_ws( header );
169  
        if( ! header.empty() )
169  
        if( ! header.empty() )
170  
        {
170  
        {
171  
            if( header.front() == ',' )
171  
            if( header.front() == ',' )
172  
                header.remove_prefix( 1 );
172  
                header.remove_prefix( 1 );
173  
            else
173  
            else
174  
                break; // Invalid
174  
                break; // Invalid
175  
        }
175  
        }
176  
    }
176  
    }
177  

177  

178  
    if( result.ranges.empty() )
178  
    if( result.ranges.empty() )
179  
    {
179  
    {
180  
        result.type = range_result_type::unsatisfiable;
180  
        result.type = range_result_type::unsatisfiable;
181  
    }
181  
    }
182  
    else if( any_satisfiable )
182  
    else if( any_satisfiable )
183  
    {
183  
    {
184  
        result.type = range_result_type::ok;
184  
        result.type = range_result_type::ok;
185  
    }
185  
    }
186  

186  

187  
    return result;
187  
    return result;
188  
}
188  
}
189  

189  

190  
} // http
190  
} // http
191  
} // boost
191  
} // boost