Line data Source code
1 : //
2 : // Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com)
3 : //
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)
6 : //
7 : // Official repository: https://github.com/cppalliance/http
8 : //
9 :
10 : #include <boost/http/server/range_parser.hpp>
11 : #include <algorithm>
12 : #include <cctype>
13 : #include <charconv>
14 :
15 : namespace boost {
16 : namespace http {
17 :
18 : namespace {
19 :
20 : // Skip whitespace
21 : void
22 0 : skip_ws( core::string_view& s ) noexcept
23 : {
24 0 : while( ! s.empty() && std::isspace(
25 0 : static_cast<unsigned char>( s.front() ) ) )
26 0 : s.remove_prefix( 1 );
27 0 : }
28 :
29 : // Parse integer
30 : bool
31 0 : parse_int( core::string_view& s, std::int64_t& out ) noexcept
32 : {
33 0 : skip_ws( s );
34 0 : if( s.empty() )
35 0 : return false;
36 :
37 0 : auto const* begin = s.data();
38 0 : auto const* end = s.data() + s.size();
39 0 : auto [ptr, ec] = std::from_chars( begin, end, out );
40 0 : if( ec != std::errc() || ptr == begin )
41 0 : return false;
42 :
43 0 : s.remove_prefix( static_cast<std::size_t>( ptr - begin ) );
44 0 : return true;
45 : }
46 :
47 : // Parse a single range spec: "start-end" or "-suffix" or "start-"
48 : bool
49 0 : parse_range_spec(
50 : core::string_view& s,
51 : std::int64_t size,
52 : byte_range& out )
53 : {
54 0 : skip_ws( s );
55 0 : if( s.empty() )
56 0 : return false;
57 :
58 0 : std::int64_t start = -1;
59 0 : std::int64_t end = -1;
60 :
61 : // Check for suffix range: "-suffix"
62 0 : if( s.front() == '-' )
63 : {
64 0 : s.remove_prefix( 1 );
65 : std::int64_t suffix;
66 0 : if( ! parse_int( s, suffix ) || suffix < 0 )
67 0 : return false;
68 :
69 : // Last 'suffix' bytes
70 0 : if( suffix == 0 )
71 0 : return false;
72 :
73 0 : if( suffix > size )
74 0 : suffix = size;
75 :
76 0 : out.start = size - suffix;
77 0 : out.end = size - 1;
78 0 : return true;
79 : }
80 :
81 : // Parse start
82 0 : if( ! parse_int( s, start ) || start < 0 )
83 0 : return false;
84 :
85 0 : skip_ws( s );
86 0 : if( s.empty() || s.front() != '-' )
87 0 : return false;
88 :
89 0 : s.remove_prefix( 1 ); // consume '-'
90 :
91 : // Check for "start-" (open-ended)
92 0 : skip_ws( s );
93 0 : if( s.empty() || s.front() == ',' )
94 : {
95 : // Open-ended: start to end of file
96 0 : out.start = start;
97 0 : out.end = size - 1;
98 0 : return start < size;
99 : }
100 :
101 : // Parse end
102 0 : if( ! parse_int( s, end ) || end < 0 )
103 0 : return false;
104 :
105 : // Validate
106 0 : if( start > end )
107 0 : return false;
108 :
109 0 : out.start = start;
110 0 : out.end = ( std::min )( end, size - 1 );
111 :
112 0 : return start < size;
113 : }
114 :
115 : } // (anon)
116 :
117 : range_result
118 0 : parse_range( std::int64_t size, core::string_view header )
119 : {
120 0 : range_result result;
121 0 : result.type = range_result_type::malformed;
122 :
123 0 : if( size <= 0 )
124 : {
125 0 : result.type = range_result_type::unsatisfiable;
126 0 : return result;
127 : }
128 :
129 : // Must start with "bytes="
130 0 : skip_ws( header );
131 0 : if( header.size() < 6 )
132 0 : return result;
133 :
134 : // Case-insensitive "bytes=" check
135 0 : auto prefix = header.substr( 0, 6 );
136 0 : bool is_bytes = true;
137 0 : for( std::size_t i = 0; i < 5; ++i )
138 : {
139 0 : char c = static_cast<char>( std::tolower(
140 0 : static_cast<unsigned char>( prefix[i] ) ) );
141 0 : if( c != "bytes"[i] )
142 : {
143 0 : is_bytes = false;
144 0 : break;
145 : }
146 : }
147 0 : if( ! is_bytes || prefix[5] != '=' )
148 0 : return result;
149 :
150 0 : header.remove_prefix( 6 );
151 :
152 : // Parse range specs
153 0 : bool any_satisfiable = false;
154 :
155 0 : while( ! header.empty() )
156 : {
157 0 : skip_ws( header );
158 0 : if( header.empty() )
159 0 : break;
160 :
161 0 : byte_range range;
162 0 : if( parse_range_spec( header, size, range ) )
163 : {
164 0 : result.ranges.push_back( range );
165 0 : any_satisfiable = true;
166 : }
167 :
168 0 : skip_ws( header );
169 0 : if( ! header.empty() )
170 : {
171 0 : if( header.front() == ',' )
172 0 : header.remove_prefix( 1 );
173 : else
174 0 : break; // Invalid
175 : }
176 : }
177 :
178 0 : if( result.ranges.empty() )
179 : {
180 0 : result.type = range_result_type::unsatisfiable;
181 : }
182 0 : else if( any_satisfiable )
183 : {
184 0 : result.type = range_result_type::ok;
185 : }
186 :
187 0 : return result;
188 0 : }
189 :
190 : } // http
191 : } // boost
|