FlatCityBuf C++ reader 0.8.0
Native C++17 reader for FlatCityBuf, the cloud-optimized CityJSON format
Loading...
Searching...
No Matches
curl_range_reader.cpp
Go to the documentation of this file.
2
3#ifdef FCB_WITH_CURL
4
5# include <algorithm>
6# include <cctype>
7# include <cstdlib>
8# include <cstring>
9
10# include <curl/curl.h>
11
12# include "../detail/checked.hpp"
13
14namespace fcb {
15
16namespace {
17
18std::size_t write_cb(char* ptr, std::size_t size, std::size_t nmemb, void* userdata) {
19 auto* out = static_cast<std::vector<std::uint8_t>*>(userdata);
20 const std::size_t n = size * nmemb;
21 out->insert(out->end(), reinterpret_cast<std::uint8_t*>(ptr),
22 reinterpret_cast<std::uint8_t*>(ptr) + n);
23 return n;
24}
25
26std::size_t header_cb(char* ptr, std::size_t size, std::size_t nmemb, void* userdata) {
27 auto* headers = static_cast<std::vector<std::string>*>(userdata);
28 headers->emplace_back(ptr, size * nmemb);
29 return size * nmemb;
30}
31
32std::string lower(std::string s) {
33 std::transform(s.begin(), s.end(), s.begin(),
34 [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
35 return s;
36}
37
38std::string find_header(const std::vector<std::string>& headers, const std::string& name) {
39 const std::string want = lower(name) + ":";
40 for (const auto& h : headers) {
41 if (lower(h).rfind(want, 0) == 0) {
42 std::string v = h.substr(want.size());
43 // trim
44 const auto b = v.find_first_not_of(" \t");
45 const auto e = v.find_last_not_of(" \t\r\n");
46 if (b == std::string::npos)
47 return {};
48 return v.substr(b, e - b + 1);
49 }
50 }
51 return {};
52}
53
55bool parse_content_range(const std::string& v, std::uint64_t& start, std::uint64_t& end,
56 std::uint64_t& total) {
57 if (v.rfind("bytes ", 0) != 0)
58 return false;
59 const std::string rest = v.substr(6);
60 const auto dash = rest.find('-');
61 const auto slash = rest.find('/');
62 if (dash == std::string::npos || slash == std::string::npos || slash < dash)
63 return false;
64 try {
65 start = std::stoull(rest.substr(0, dash));
66 end = std::stoull(rest.substr(dash + 1, slash - dash - 1));
67 const std::string t = rest.substr(slash + 1);
68 if (t == "*")
69 return false;
70 total = std::stoull(t);
71 } catch (...) {
72 return false;
73 }
74 return true;
75}
76
77} // namespace
78
80 std::string url;
82 CURL* easy = nullptr;
83 bool have_size = false;
84 std::uint64_t size = 0;
85 std::string validator; // ETag or Last-Modified value
86 bool validator_is_etag = false;
87 struct curl_slist* extra_headers = nullptr;
88
90 if (extra_headers != nullptr)
91 curl_slist_free_all(extra_headers);
92 if (easy != nullptr)
93 curl_easy_cleanup(easy);
94 }
95
96 void apply_common() {
97 curl_easy_setopt(easy, CURLOPT_URL, url.c_str());
98 curl_easy_setopt(easy, CURLOPT_TIMEOUT_MS, options.timeout_ms);
99 curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT_MS, options.connect_timeout_ms);
100 curl_easy_setopt(easy, CURLOPT_FOLLOWLOCATION, options.follow_redirects ? 1L : 0L);
101 curl_easy_setopt(easy, CURLOPT_USERAGENT, options.user_agent.c_str());
102 // A compressed representation makes byte ranges meaningless.
103 curl_easy_setopt(easy, CURLOPT_ACCEPT_ENCODING, "identity");
104 curl_easy_setopt(easy, CURLOPT_NOSIGNAL, 1L);
105 }
106
108 if (extra_headers != nullptr) {
109 curl_slist_free_all(extra_headers);
110 extra_headers = nullptr;
111 }
113 const std::string h = validator_is_etag ? ("If-Match: " + validator)
114 : ("If-Unmodified-Since: " + validator);
115 extra_headers = curl_slist_append(extra_headers, h.c_str());
116 }
117 curl_easy_setopt(easy, CURLOPT_HTTPHEADER, extra_headers);
118 }
119
120 void capture_validator(const std::vector<std::string>& headers) {
121 if (!validator.empty())
122 return;
123 const std::string etag = find_header(headers, "ETag");
124 if (!etag.empty()) {
125 validator = etag;
126 validator_is_etag = true;
127 return;
128 }
129 const std::string lm = find_header(headers, "Last-Modified");
130 if (!lm.empty()) {
131 validator = lm;
132 validator_is_etag = false;
133 }
134 }
135};
136
137CurlRangeReader::CurlRangeReader(const std::string& url, CurlOptions options)
138 : impl_(std::make_unique<Impl>()) {
139 impl_->url = url;
140 impl_->options = std::move(options);
141
142 // Reuse one easy handle across every request: connection reuse and
143 // keepalive are where the latency win is, and the traversal already
144 // coalesces ranges, so curl_multi would add concurrency the access
145 // pattern cannot exploit.
146 impl_->easy = curl_easy_init();
147 if (impl_->easy == nullptr) {
148 throw Error(ErrorCode::HttpError, "curl_easy_init failed");
149 }
150 impl_->apply_common();
151}
152
154
156 if (impl_->have_size)
157 return impl_->size;
158
159 std::vector<std::string> headers;
160 std::vector<std::uint8_t> body;
161
162 curl_easy_reset(impl_->easy);
163 impl_->apply_common();
164 curl_easy_setopt(impl_->easy, CURLOPT_NOBODY, 1L);
165 curl_easy_setopt(impl_->easy, CURLOPT_HEADERFUNCTION, header_cb);
166 curl_easy_setopt(impl_->easy, CURLOPT_HEADERDATA, &headers);
167 curl_easy_setopt(impl_->easy, CURLOPT_WRITEFUNCTION, write_cb);
168 curl_easy_setopt(impl_->easy, CURLOPT_WRITEDATA, &body);
169
170 ++request_count_;
171 const CURLcode rc = curl_easy_perform(impl_->easy);
172 if (rc != CURLE_OK) {
173 throw Error(ErrorCode::HttpError, std::string("HEAD failed: ") + curl_easy_strerror(rc));
174 }
175
176 long status = 0;
177 curl_easy_getinfo(impl_->easy, CURLINFO_RESPONSE_CODE, &status);
178 if (status < 200 || status >= 300) {
179 throw Error(ErrorCode::HttpError, "HEAD returned status " + std::to_string(status));
180 }
181
182 curl_off_t len = -1;
183 curl_easy_getinfo(impl_->easy, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, &len);
184 if (len < 0) {
185 // Some servers omit Content-Length on HEAD. Fall back to a one-byte
186 // range and read the total out of Content-Range.
187 auto probe = read(0, 1);
188 if (!impl_->have_size) {
189 throw Error(ErrorCode::HttpError, "server did not report a resource size");
190 }
191 return impl_->size;
192 }
193
194 impl_->capture_validator(headers);
195 impl_->size = static_cast<std::uint64_t>(len);
196 impl_->have_size = true;
197 return impl_->size;
198}
199
200std::vector<std::uint8_t> CurlRangeReader::read(std::uint64_t offset, std::uint64_t length) {
201 if (length == 0)
202 return {}; // contract: never contact the transport
203
204 const std::uint64_t last = detail::range_end(offset, length) - 1;
205 const std::string range = std::to_string(offset) + "-" + std::to_string(last);
206
207 std::vector<std::string> headers;
208 std::vector<std::uint8_t> body;
209
210 curl_easy_reset(impl_->easy);
211 impl_->apply_common();
212 impl_->rebuild_headers();
213 curl_easy_setopt(impl_->easy, CURLOPT_RANGE, range.c_str());
214 curl_easy_setopt(impl_->easy, CURLOPT_HEADERFUNCTION, header_cb);
215 curl_easy_setopt(impl_->easy, CURLOPT_HEADERDATA, &headers);
216 curl_easy_setopt(impl_->easy, CURLOPT_WRITEFUNCTION, write_cb);
217 curl_easy_setopt(impl_->easy, CURLOPT_WRITEDATA, &body);
218
219 ++request_count_;
220 const CURLcode rc = curl_easy_perform(impl_->easy);
221 if (rc != CURLE_OK) {
223 std::string("range request failed: ") + curl_easy_strerror(rc));
224 }
225
226 long status = 0;
227 curl_easy_getinfo(impl_->easy, CURLINFO_RESPONSE_CODE, &status);
228 impl_->capture_validator(headers);
229
230 if (status == 412) {
231 throw Error(ErrorCode::HttpError, "resource changed between requests (If-Match failed); "
232 "the URL is not stable");
233 }
234
235 if (status == 416) {
236 // Unsatisfiable. Legitimate only when reading at or past the end.
237 if (impl_->have_size && offset >= impl_->size)
238 return {};
239 throw Error(ErrorCode::HttpError, "server returned 416 for an in-range request");
240 }
241
242 if (status == 206) {
243 // A server may legally answer with a DIFFERENT range than asked for,
244 // so never assume the body corresponds to the request.
245 const std::string cr = find_header(headers, "Content-Range");
246 std::uint64_t s = 0, e = 0, total = 0;
247 if (!parse_content_range(cr, s, e, total)) {
248 throw Error(ErrorCode::HttpError, "malformed or missing Content-Range on 206");
249 }
250 if (s != offset) {
251 throw Error(ErrorCode::HttpError, "server returned range starting at " +
252 std::to_string(s) + ", expected " +
253 std::to_string(offset));
254 }
255 if (body.size() != (e - s + 1)) {
256 throw Error(ErrorCode::HttpError, "206 body length disagrees with Content-Range");
257 }
258 if (!impl_->have_size) {
259 impl_->size = total;
260 impl_->have_size = true;
261 }
262 // Short only where the range crossed EOF; anything else is truncation.
263 if (body.size() < length && (offset + body.size()) < impl_->size) {
264 throw Error(ErrorCode::HttpError, "truncated 206 response");
265 }
266 return body;
267 }
268
269 if (status == 200) {
270 // The server ignored Range and sent the whole representation. Do NOT
271 // truncate to `length` -- that returns bytes [0, length), not
272 // [offset, offset+length). Slice properly instead.
273 if (!impl_->have_size) {
274 impl_->size = body.size();
275 impl_->have_size = true;
276 }
277 if (offset >= body.size())
278 return {};
279 const std::uint64_t avail = body.size() - offset;
280 const std::uint64_t n = std::min<std::uint64_t>(length, avail);
281 return std::vector<std::uint8_t>(body.begin() + static_cast<std::ptrdiff_t>(offset),
282 body.begin() + static_cast<std::ptrdiff_t>(offset + n));
283 }
284
285 throw Error(ErrorCode::HttpError, "unexpected HTTP status " + std::to_string(status));
286}
287
288} // namespace fcb
289
290#endif // FCB_WITH_CURL
std::vector< std::uint8_t > read(std::uint64_t offset, std::uint64_t length) override
Read length bytes at offset, subject to the contract above.
~CurlRangeReader() override
std::uint64_t total_size() override
Total byte length of the resource.
CurlRangeReader(const std::string &url, CurlOptions options={})
Every failure the library reports is one of these.
Definition error.hpp:30
std::uint64_t range_end(std::uint64_t offset, std::uint64_t length)
End of a range, checked.
Definition checked.hpp:44
std::uint64_t offset
Definition stree.cpp:57
bool require_stable_representation
Require the server to prove the representation has not changed between requests (ETag/If-Match,...
struct curl_slist * extra_headers
void capture_validator(const std::vector< std::string > &headers)