libs/http/src/server/range_parser.cpp

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