FlatCityBuf C++ reader 0.8.0
Native C++17 reader for FlatCityBuf, the cloud-optimized CityJSON format
Loading...
Searching...
No Matches
reader.cpp
Go to the documentation of this file.
1#include <fcb/attribute.hpp>
2#include <fcb/generated/feature_generated.h>
3#include <fcb/generated/header_generated.h>
5#include <fcb/reader.hpp>
6#include <fcb/stree.hpp>
7
8#include <algorithm>
9#include <cstring>
10#include <utility>
11
12#include "detail/checked.hpp"
14
15namespace fcb {
16
18static constexpr std::size_t kBodyAlignPad = 0;
19
20// -------------------------------------------------------------- Feature ---
21
22Feature::Feature(std::shared_ptr<const std::vector<std::uint8_t>> buffer, std::uint64_t byte_offset,
23 std::size_t body_offset)
24 : buffer_(std::move(buffer)), byte_offset_(byte_offset), body_offset_(body_offset) {}
25
26const ::CityFeature* Feature::raw() const {
27 if (buffer_ == nullptr)
28 return nullptr;
29 return GetSizePrefixedCityFeature(buffer_->data() + body_offset_);
30}
31
32const ::CityFeature* detail::FeatureAccess::get(const Feature& f) { return f.raw(); }
33
34std::string Feature::id() const {
35 const ::CityFeature* cf = raw();
36 if (cf == nullptr || cf->id() == nullptr)
37 return {};
38 return cf->id()->str();
39}
40
41namespace {
42const ::CityObject* object_at(const ::CityFeature* cf, std::size_t i) {
43 if (cf == nullptr || cf->objects() == nullptr)
44 return nullptr;
45 if (i >= cf->objects()->size())
46 return nullptr;
47 return cf->objects()->Get(static_cast<flatbuffers::uoffset_t>(i));
48}
49} // namespace
50
52 const auto* obj = object_at(raw(), i);
53 if (obj == nullptr || obj->attributes() == nullptr)
54 return {};
55 const auto* a = obj->attributes();
56 return bytes_view(a->data(), a->size());
57}
58
59bool Feature::object_has_attributes(std::size_t i) const {
60 const auto* obj = object_at(raw(), i);
61 return obj != nullptr && obj->attributes() != nullptr;
62}
63
64bool Feature::object_extent(std::size_t i, std::array<double, 6>& out) const {
65 const auto* obj = object_at(raw(), i);
66 if (obj == nullptr || obj->geographical_extent() == nullptr)
67 return false;
68 const auto* e = obj->geographical_extent();
69 // memcpy-based reads: these structs can sit at misaligned internal
70 // offsets, same as Transform in the header. See header.cpp.
71 auto rd = [](const void* base, std::size_t off) {
72 double d;
73 std::memcpy(&d, static_cast<const std::uint8_t*>(base) + off, sizeof(double));
74 return d;
75 };
76 out = {rd(e, 0), rd(e, 8), rd(e, 16), rd(e, 24), rd(e, 32), rd(e, 40)};
77 return true;
78}
79
80bool Feature::object_has_columns(std::size_t i) const {
81 const auto* obj = object_at(raw(), i);
82 return obj != nullptr && obj->columns() != nullptr;
83}
84
85std::vector<ColumnInfo> Feature::object_columns(std::size_t i) const {
86 std::vector<ColumnInfo> out;
87 const auto* obj = object_at(raw(), i);
88 if (obj == nullptr || obj->columns() == nullptr)
89 return out;
90 out.reserve(obj->columns()->size());
91 for (const auto* c : *obj->columns()) {
92 if (c == nullptr)
93 continue;
94 ColumnInfo ci{};
95 ci.index = c->index();
96 ci.name = c->name() != nullptr ? c->name()->str() : std::string();
97 ci.type = static_cast<std::uint8_t>(c->type());
98 ci.nullable = c->nullable();
99 out.push_back(std::move(ci));
100 }
101 return out;
102}
103
104std::string Feature::object_id(std::size_t i) const {
105 const auto* obj = object_at(raw(), i);
106 if (obj == nullptr || obj->id() == nullptr)
107 return {};
108 return obj->id()->str();
109}
110
111std::size_t Feature::city_object_count() const {
112 const ::CityFeature* cf = raw();
113 if (cf == nullptr || cf->objects() == nullptr)
114 return 0;
115 return cf->objects()->size();
116}
117
118// ------------------------------------------------------- FeatureIterator ---
119
120FeatureIterator::FeatureIterator(std::shared_ptr<RangeReader> reader, HeaderView header,
121 IterationMode mode, std::vector<SearchResultItem> hits)
122 : reader_(std::move(reader)), header_(std::move(header)), mode_(mode), hits_(std::move(hits)) {
123 cursor_ = header_.layout().feature_begin;
124}
125
127 const std::uint64_t features_count = header_.info().features_count;
128 const std::uint64_t total_size = reader_->total_size();
129
130 std::uint64_t at = 0;
131 if (mode_ == IterationMode::SequentialScan) {
132 // features_count == 0 means UNKNOWN (header.fbs), not empty. With a
133 // known count we stop after exactly that many; with an unknown count
134 // we run to EOF, as the reference does (reader/mod.rs:488-498).
135 const bool known = features_count > 0;
136 if (known && produced_ >= features_count) {
137 current_ = Feature();
138 // A known count that leaves bytes behind means the file claims
139 // fewer features than it carries.
140 if (cursor_ < total_size) {
142 "trailing bytes after " + std::to_string(features_count) + " features");
143 }
144 return false;
145 }
146 if (!known && cursor_ >= total_size) {
147 current_ = Feature();
148 return false;
149 }
150 at = cursor_;
151 } else {
152 if (hit_index_ >= hits_.size()) {
153 current_ = Feature();
154 return false;
155 }
156 at = detail::checked_add(header_.layout().feature_begin, hits_[hit_index_].offset,
157 "feature offset");
158 ++hit_index_;
159 }
160
161 // Validate the offset before touching the transport: a hostile leaf
162 // offset must not cause an out-of-resource request.
163 if (at >= total_size) {
164 throw Error(ErrorCode::IoError, "feature offset past end of resource");
165 }
166
167 auto prefix = reader_->read(at, 4);
168 if (prefix.size() < 4) {
169 // Reaching EOF before features_count features is a TRUNCATED file,
170 // not a clean end of iteration. Accepting it silently would let a
171 // file cut in half read as a valid short file.
172 if (features_count == 0) {
173 // Unknown count: a short read at the end is the normal terminus.
174 current_ = Feature();
175 return false;
176 }
177 throw Error(ErrorCode::IoError, "truncated feature section: expected " +
178 std::to_string(features_count) + " features, got " +
179 std::to_string(produced_));
180 }
181
182 const std::uint32_t len = static_cast<std::uint32_t>(prefix[0]) |
183 (static_cast<std::uint32_t>(prefix[1]) << 8) |
184 (static_cast<std::uint32_t>(prefix[2]) << 16) |
185 (static_cast<std::uint32_t>(prefix[3]) << 24);
186
187 // Bound the allocation BEFORE making it: a crafted 0xFFFFFFFF prefix
188 // would otherwise ask for ~4 GiB.
189 if (len == 0 || len > kMaxFeatureSize) {
191 "implausible feature size: " + std::to_string(len));
192 }
193 const std::uint64_t want = detail::checked_add(4, len, "feature length");
194 detail::require_within(at, want, total_size, "feature body");
195
196 auto raw_buf = reader_->read(at, want);
197 if (raw_buf.size() < want) {
198 throw Error(ErrorCode::IoError, "truncated feature body");
199 }
200
201 auto buf = std::make_shared<std::vector<std::uint8_t>>(kBodyAlignPad + raw_buf.size());
202 std::copy(raw_buf.begin(), raw_buf.end(), buf->begin() + kBodyAlignPad);
203
204 // Full structural verification, alignment included; see header.cpp.
205 flatbuffers::Verifier verifier(buf->data() + kBodyAlignPad, buf->size() - kBodyAlignPad);
206 if (!VerifySizePrefixedCityFeatureBuffer(verifier)) {
208 "feature failed FlatBuffers verification at offset " + std::to_string(at));
209 }
210
211 current_ = Feature(std::const_pointer_cast<const std::vector<std::uint8_t>>(buf),
212 at - header_.layout().feature_begin, kBodyAlignPad);
213
214 if (mode_ == IterationMode::SequentialScan) {
215 cursor_ = detail::checked_add(at, want, "feature cursor");
216 }
217 ++produced_;
218 return true;
219}
220
221// ------------------------------------------------------------ FcbReader ---
222
223FcbReader::FcbReader(std::shared_ptr<RangeReader> reader, HeaderView header)
224 : reader_(std::move(reader)), header_(std::move(header)) {}
225
226FcbReader FcbReader::open_file(const std::string& path) {
227 return open(std::make_shared<FileRangeReader>(path));
228}
229
230FcbReader FcbReader::open(std::shared_ptr<RangeReader> reader) {
232 return FcbReader(std::move(reader), std::move(header));
233}
234
236 const auto& info = header_.info();
237 const auto& layout = header_.layout();
238
239 if (layout.rtree_size == 0 || info.features_count == 0) {
240 throw Error(ErrorCode::NoIndex, "file has no spatial index");
241 }
242
243 // Index traversal gets its own buffering window. The Rust HTTP reader
244 // coalesces node ranges up to 256 KB (http_reader/mod.rs:213); a window
245 // of that size gives the same effect through the decorator.
246 auto index_reader = std::make_shared<BufferedRangeReader>(reader_, 256 * 1024);
247 auto hits = rtree_search_bbox(*index_reader, layout.rtree_begin, info.features_count,
248 info.index_node_size, query);
249
250 auto feature_reader = std::make_shared<BufferedRangeReader>(reader_, 1048576);
251 return FeatureIterator(std::move(feature_reader), header_, IterationMode::OffsetList,
252 std::move(hits));
253}
254
255namespace {
256
258bool value_satisfies(const AttrValue& v, Operator op, const KeyValue& want, KeyKind kind) {
259 KeyValue actual;
260 switch (v.type) {
262 actual = KeyValue::from_bool(v.b);
263 break;
265 actual = KeyValue::from_i64(v.i);
266 break;
268 actual = KeyValue::from_u64(v.u);
269 break;
271 actual = KeyValue::from_f64(v.d);
272 break;
275 // Compare the FULL strings, not the truncated keys -- this is the
276 // whole point of post-filtering.
277 {
278 const std::string& a = v.s;
279 const std::string& b = want.original_string();
280 const int c = a.compare(b);
281 switch (op) {
282 case Operator::Eq:
283 return c == 0;
284 case Operator::Ne:
285 return c != 0;
286 case Operator::Gt:
287 return c > 0;
288 case Operator::Ge:
289 return c >= 0;
290 case Operator::Lt:
291 return c < 0;
292 case Operator::Le:
293 return c <= 0;
294 }
295 return false;
296 }
298 return false;
299 }
300
301 // Numeric kinds: coerce both sides to the query's kind before comparing.
302 if (actual.kind() != want.kind()) {
303 switch (kind) {
304 case KeyKind::Float32:
305 case KeyKind::Float64:
306 actual = KeyValue::from_f64(
307 v.type == AttrValue::Type::Double ? v.d : static_cast<double>(v.i));
308 break;
309 default:
312 if (actual.kind() != want.kind())
313 return false;
314 break;
315 }
316 if (actual.kind() != want.kind())
317 return false;
318 }
319
320 const int c = compare_keys(actual, want);
321 switch (op) {
322 case Operator::Eq:
323 return c == 0;
324 case Operator::Ne:
325 return c != 0;
326 case Operator::Gt:
327 return c > 0;
328 case Operator::Ge:
329 return c >= 0;
330 case Operator::Lt:
331 return c < 0;
332 case Operator::Le:
333 return c <= 0;
334 }
335 return false;
336}
337
338bool needs_post_filter(KeyKind kind) {
340}
341
342} // namespace
343
345 if (query.empty()) {
346 throw Error(ErrorCode::QueryExecutionError, "empty attribute query");
347 }
348
349 auto index_reader = std::make_shared<BufferedRangeReader>(reader_, 1024 * 1024);
350
351 std::vector<SearchResultItem> acc;
352 bool first = true;
353 bool any_post_filter = false;
354
355 for (const auto& cond : query) {
356 // Resolve the column and its index.
357 const ColumnInfo* col = nullptr;
358 for (const auto& c : header_.info().columns) {
359 if (c.name == cond.field) {
360 col = &c;
361 break;
362 }
363 }
364 if (col == nullptr) {
365 throw Error(ErrorCode::AttributeIndexNotFound, "no such column: " + cond.field);
366 }
367
368 // Json/Binary keys are the first 100 bytes of a serialized blob:
369 // an index hit says nothing about the actual (undecoded) value, so
370 // answering the query would be dishonest. Checked before the
371 // "is it indexed" lookup below, so the rejection does not depend on
372 // whether this particular writer happened to index the column.
373 // This is one of the four deliberate Rust/C++/Python divergences
374 // from the plan's writer defaults; matches
375 // fcb_core/src/reader/attr_query.rs's catch-all
376 // `Err(Error::UnsupportedColumnType(...))` for any column type its
377 // index builder does not special-case (Json and Binary fall through
378 // to it), and stree.py's `_resolve` (DIVERGENCE 2).
379 if (col->type == static_cast<std::uint8_t>(::ColumnType::Json) ||
380 col->type == static_cast<std::uint8_t>(::ColumnType::Binary)) {
382 "column " + cond.field +
383 " is Json/Binary: its index is a fixed-width key "
384 "over a blob, so hits are meaningless without "
385 "post-verification");
386 }
387
388 const AttrIndexInfo* idx = nullptr;
389 for (const auto& a : header_.attr_indices()) {
390 if (a.column_index == col->index) {
391 idx = &a;
392 break;
393 }
394 }
395 if (idx == nullptr) {
396 throw Error(ErrorCode::AttributeIndexNotFound, "column is not indexed: " + cond.field);
397 }
398
399 const KeyKind kind = key_kind_for_column(col->type);
400 if (needs_post_filter(kind))
401 any_post_filter = true;
402
403 auto hits = stree_query(*index_reader, *idx, kind, cond.op, cond.value);
404
405 std::sort(hits.begin(), hits.end(),
406 [](const SearchResultItem& a, const SearchResultItem& b) {
407 return a.offset < b.offset;
408 });
409 hits.erase(std::unique(hits.begin(), hits.end(),
410 [](const SearchResultItem& a, const SearchResultItem& b) {
411 return a.offset == b.offset;
412 }),
413 hits.end());
414
415 if (first) {
416 acc = std::move(hits);
417 first = false;
418 } else {
419 // AND: intersect on feature offset, with early exit.
420 std::vector<SearchResultItem> merged;
421 std::set_intersection(acc.begin(), acc.end(), hits.begin(), hits.end(),
422 std::back_inserter(merged),
423 [](const SearchResultItem& a, const SearchResultItem& b) {
424 return a.offset < b.offset;
425 });
426 acc = std::move(merged);
427 }
428 if (acc.empty())
429 break;
430 }
431
432 auto feature_reader = std::make_shared<BufferedRangeReader>(reader_, 1048576);
433
434 if (opts.exact_index_only || !any_post_filter || acc.empty()) {
435 return FeatureIterator(std::move(feature_reader), header_, IterationMode::OffsetList,
436 std::move(acc));
437 }
438
439 // Post-filter: fixed-width string keys collide, so the tree gave us
440 // candidates. Verify each against the decoded, untruncated attribute.
441 // Zero padding means even SHORT queries can collide ("a" vs "a "), so
442 // this is not gated on the query length.
443 std::vector<SearchResultItem> verified;
444 FeatureIterator probe(feature_reader, header_, IterationMode::OffsetList, acc);
445 while (probe.next()) {
446 const Feature& f = probe.current();
447 bool ok = true;
448 for (const auto& cond : query) {
449 const ColumnInfo* col = nullptr;
450 for (const auto& c : header_.info().columns) {
451 if (c.name == cond.field) {
452 col = &c;
453 break;
454 }
455 }
456 if (col == nullptr) {
457 ok = false;
458 break;
459 }
460 const KeyKind kind = key_kind_for_column(col->type);
461 if (!needs_post_filter(kind))
462 continue;
463
464 // Existential over CityObjects: attributes may live on any of
465 // them, each with its own column schema.
466 bool matched = false;
467 for (std::size_t i = 0; i < f.city_object_count() && !matched; ++i) {
468 auto blob = f.object_attributes(i);
469 if (blob.empty())
470 continue;
471 auto own = f.object_columns(i);
472 const auto& schema = f.object_has_columns(i) ? own : header_.info().columns;
473 for (auto& [name, val] : decode_attributes(blob, schema)) {
474 if (name != cond.field)
475 continue;
476 if (value_satisfies(val, cond.op, cond.value, kind))
477 matched = true;
478 break;
479 }
480 }
481 if (!matched) {
482 ok = false;
483 break;
484 }
485 }
486 if (ok) {
487 verified.push_back(SearchResultItem{f.byte_offset(), 0});
488 }
489 }
490
491 auto out_reader = std::make_shared<BufferedRangeReader>(reader_, 1048576);
492 return FeatureIterator(std::move(out_reader), header_, IterationMode::OffsetList,
493 std::move(verified));
494}
495
497 // Per-query buffering at the feature-phase window size, matching
498 // DEFAULT_HTTP_FETCH_SIZE in http_reader/mod.rs:42. Constructed fresh
499 // per query so concurrent iterators cannot disturb each other.
500 auto buffered = std::make_shared<BufferedRangeReader>(reader_, 1048576);
501 return FeatureIterator(std::move(buffered), header_, IterationMode::SequentialScan, {});
502}
503
504} // namespace fcb
Every failure the library reports is one of these.
Definition error.hpp:30
The library's entry point.
Definition reader.hpp:67
FeatureIterator select_bbox(const BBox &query)
Iterate features whose 2D bounding box intersects query.
Definition reader.cpp:235
static FcbReader open(std::shared_ptr< RangeReader > reader)
Definition reader.cpp:230
FeatureIterator select_all()
Iterate every feature in stored (Hilbert) order.
Definition reader.cpp:496
const HeaderView & header() const
Definition reader.hpp:72
FeatureIterator select_attr(const AttrQuery &query, AttrQueryOptions opts={})
Iterate features matching every condition (AND).
Definition reader.cpp:344
static FcbReader open_file(const std::string &path)
Definition reader.cpp:226
Single-pass iterator over features. Not copyable.
Definition reader.hpp:35
bool next()
Advance.
Definition reader.cpp:126
std::uint64_t features_count() const
Total features the header claims, for progress reporting.
Definition reader.hpp:52
const Feature & current() const
Definition reader.hpp:49
FeatureIterator(std::shared_ptr< RangeReader > reader, HeaderView header, IterationMode mode, std::vector< SearchResultItem > hits)
Definition reader.cpp:120
One decoded feature that OWNS the bytes it points into.
Definition feature.hpp:33
bool object_extent(std::size_t i, std::array< double, 6 > &out) const
CityObject i's own bounding box, if it declares one.
Definition reader.cpp:64
bool object_has_attributes(std::size_t i) const
Whether CityObject i declares an attributes vector at all.
Definition reader.cpp:59
bytes_view object_attributes(std::size_t i) const
Raw attribute blob of CityObject i, or empty if it has none.
Definition reader.cpp:51
const ::CityFeature * raw() const
The generated CityFeature table behind this Feature.
Definition reader.cpp:26
std::uint64_t byte_offset() const
Byte offset of this feature RELATIVE to the start of the features section, matching the offsets store...
Definition feature.hpp:81
std::string object_id(std::size_t i) const
CityObject i's id.
Definition reader.cpp:104
bool object_has_columns(std::size_t i) const
CityObject i's own column schema, if it declares one.
Definition reader.cpp:80
std::string id() const
The feature's CityJSON id. Empty when empty().
Definition reader.cpp:34
Feature()=default
std::vector< ColumnInfo > object_columns(std::size_t i) const
Definition reader.cpp:85
std::size_t city_object_count() const
How many CityObjects this feature carries. Zero when empty().
Definition reader.cpp:111
A parsed header that OWNS its backing bytes.
Definition header.hpp:108
const std::vector< AttrIndexInfo > & attr_indices() const
Definition header.hpp:114
const FileLayout & layout() const
Definition header.hpp:113
const FileInfo & info() const
Definition header.hpp:112
A decoded index key.
Definition key.hpp:40
static KeyValue from_i64(std::int64_t v)
Definition key.cpp:104
const std::string & original_string() const
The original, untruncated string this key was built from.
Definition key.hpp:62
KeyKind kind() const
Definition key.hpp:58
static KeyValue from_f64(double v)
Definition key.cpp:115
static KeyValue from_u64(std::uint64_t v)
Definition key.cpp:105
static KeyValue from_bool(bool v)
Definition key.cpp:122
Minimal C++17 stand-in for std::span: a non-owning view over contiguous memory.
Definition span.hpp:13
void require_within(std::uint64_t offset, std::uint64_t length, std::uint64_t limit, const char *what)
Throws unless [offset, offset+length) lies wholly within limit.
Definition checked.hpp:49
std::uint64_t checked_add(std::uint64_t a, std::uint64_t b, const char *what="add")
Definition checked.hpp:16
constexpr std::uint64_t kMaxFeatureSize
Hard ceiling on a single feature's byte length, enforced before allocating.
Definition layout.hpp:20
span< const std::uint8_t > bytes_view
The workhorse alias: a read-only view over bytes.
Definition span.hpp:43
std::vector< SearchResultItem > rtree_search_bbox(RangeReader &reader, std::uint64_t index_begin, std::uint64_t num_items, std::uint16_t node_size, const BBox &query)
Breadth-first bbox search over the packed R-tree, reading nodes through the supplied reader.
std::vector< SearchResultItem > stree_query(RangeReader &reader, const AttrIndexInfo &index, KeyKind kind, Operator op, const KeyValue &value)
Run one condition against one column's index blob, returning candidate feature offsets (relative to t...
Definition stree.cpp:360
IterationMode
How a FeatureIterator decides what to visit.
Definition reader.hpp:29
@ SequentialScan
walk the features section start to finish
@ OffsetList
visit exactly the offsets supplied (possibly none)
KeyKind key_kind_for_column(std::uint8_t column_type)
Column type to key kind, following what the WRITER emits.
Definition key.cpp:387
HeaderView read_header(std::shared_ptr< RangeReader > reader)
Read and validate the file preamble and header.
Definition header.cpp:199
int compare_keys(const KeyValue &a, const KeyValue &b)
Three-way comparison.
Definition key.cpp:272
KeyKind
The concrete key types the B+tree index can hold.
Definition key.hpp:14
std::vector< AttrCondition > AttrQuery
Definition stree.hpp:26
std::vector< std::pair< std::string, AttrValue > > decode_attributes(bytes_view blob, const std::vector< ColumnInfo > &schema)
Decode a feature's attribute blob against the column schema.
Definition attribute.cpp:47
Operator
Comparison operators the attribute index supports.
Definition stree.hpp:17
static constexpr std::size_t kBodyAlignPad
No padding needed.
Definition header.cpp:20
KeyKind kind
Definition stree.cpp:166
RangeReader & reader
Definition stree.cpp:162
Where one column's B+tree index lives, and how it is shaped.
Definition header.hpp:35
bool exact_index_only
Return raw index candidates without verifying them against the decoded attribute.
Definition stree.hpp:33
One decoded attribute value.
Definition attribute.hpp:23
std::uint64_t u
Definition attribute.hpp:38
std::int64_t i
Definition attribute.hpp:37
std::string s
Definition attribute.hpp:40
A 2D query rectangle.
One attribute column's schema, copied out of the header.
Definition header.hpp:27
std::uint16_t index
Definition header.hpp:28
std::uint8_t type
Definition header.hpp:30
std::uint64_t features_count
Definition header.hpp:45
std::vector< ColumnInfo > columns
Definition header.hpp:47
std::uint64_t feature_begin
Definition layout.hpp:38
One hit from an index traversal.
Definition reader.hpp:18
static const ::CityFeature * get(const Feature &f)
Definition reader.cpp:32
bool ok
Definition attribute.cpp:28