FlatCityBuf C++ reader 0.8.0
Native C++17 reader for FlatCityBuf, the cloud-optimized CityJSON format
Loading...
Searching...
No Matches
header.cpp
Go to the documentation of this file.
1#include <fcb/generated/header_generated.h>
2#include <fcb/header.hpp>
3
4#include <algorithm>
5#include <cstring>
6#include <string>
7
8#include "detail/checked.hpp"
10
11namespace fcb {
12
20static constexpr std::size_t kBodyAlignPad = 0;
21
22const ::Header* HeaderView::raw() const {
23 // A default-constructed HeaderView owns no bytes. Report that rather
24 // than dereferencing the empty shared_ptr, as Feature::raw() does.
25 if (buffer_ == nullptr)
26 return nullptr;
27 return GetSizePrefixedHeader(buffer_->data() + kBodyAlignPad);
28}
29
30const ::Header* detail::HeaderAccess::get(const HeaderView& h) { return h.raw(); }
31
32namespace {
33
45double read_f64_at(const void* base, std::size_t byte_offset) {
46 double d;
47 std::memcpy(&d, static_cast<const std::uint8_t*>(base) + byte_offset, sizeof(double));
48 return d; // FlatBuffers scalars are little-endian; so is every target we support.
49}
50
53std::uint32_t read_u32_le(const std::vector<std::uint8_t>& b, std::size_t at) {
54 return static_cast<std::uint32_t>(b[at]) | (static_cast<std::uint32_t>(b[at + 1]) << 8) |
55 (static_cast<std::uint32_t>(b[at + 2]) << 16) |
56 (static_cast<std::uint32_t>(b[at + 3]) << 24);
57}
58
59void collect_columns(const flatbuffers::Vector<flatbuffers::Offset<::Column>>* cols,
60 std::vector<ColumnInfo>& out) {
61 if (cols == nullptr)
62 return;
63 out.reserve(cols->size());
64 for (const auto* c : *cols) {
65 if (c == nullptr)
66 continue;
67 ColumnInfo ci{};
68 ci.index = c->index();
69 ci.name = c->name() != nullptr ? c->name()->str() : std::string();
70 ci.type = static_cast<std::uint8_t>(c->type());
71 ci.nullable = c->nullable();
72 out.push_back(std::move(ci));
73 }
74}
75
76void fill_columns(const ::Header* hdr, FileInfo& info) {
77 collect_columns(hdr->columns(), info.columns);
78 collect_columns(hdr->semantic_columns(), info.semantic_columns);
79}
80
81void fill_metadata(const ::Header* hdr, FileInfo& info) {
82 info.features_count = hdr->features_count();
83 info.index_node_size = hdr->index_node_size();
84
85 // Transform = { Vector scale; Vector translate; }, Vector = 3 doubles.
86 // Read via memcpy: see read_f64_at() for why the accessors are unsafe.
87 if (const auto* t = hdr->transform()) {
88 info.has_transform = true;
89 info.scale = {read_f64_at(t, 0), read_f64_at(t, 8), read_f64_at(t, 16)};
90 info.translate = {read_f64_at(t, 24), read_f64_at(t, 32), read_f64_at(t, 40)};
91 }
92
93 // GeographicalExtent = { Vector min; Vector max; }.
94 if (const auto* e = hdr->geographical_extent()) {
95 info.has_extent = true;
96 info.geographical_extent = {read_f64_at(e, 0), read_f64_at(e, 8), read_f64_at(e, 16),
97 read_f64_at(e, 24), read_f64_at(e, 32), read_f64_at(e, 40)};
98 }
99
100 if (const auto* rs = hdr->reference_system()) {
101 const std::string authority =
102 rs->authority() != nullptr ? rs->authority()->str() : std::string("EPSG");
103 if (rs->code() != 0) {
104 info.crs = authority + ":" + std::to_string(rs->code());
105 } else if (rs->code_string() != nullptr) {
106 info.crs = authority + ":" + rs->code_string()->str();
107 }
108 }
109
110 if (hdr->version() != nullptr)
111 info.cityjson_version = hdr->version()->str();
112
113 // Every `!= nullptr` guard below IS the presence test, and the target is a
114 // std::optional so it survives into FileInfo: `nullptr` maps to Rust's
115 // `None` (key omitted downstream), a present-but-empty string to
116 // `Some("")` (key emitted as ""). Collapsing the two into a plain
117 // std::string was upstream finding #20.11.
118 if (hdr->identifier() != nullptr)
119 info.identifier = hdr->identifier()->str();
120 if (hdr->title() != nullptr)
121 info.title = hdr->title()->str();
122 if (hdr->reference_date() != nullptr)
123 info.reference_date = hdr->reference_date()->str();
124
125 if (hdr->poc_contact_name() != nullptr) {
126 info.poc_contact_name = hdr->poc_contact_name()->str();
127 }
128 if (hdr->poc_contact_type() != nullptr) {
129 info.poc_contact_type = hdr->poc_contact_type()->str();
130 }
131 if (hdr->poc_role() != nullptr)
132 info.poc_role = hdr->poc_role()->str();
133 if (hdr->poc_phone() != nullptr)
134 info.poc_phone = hdr->poc_phone()->str();
135 if (hdr->poc_email() != nullptr)
136 info.poc_email = hdr->poc_email()->str();
137 if (hdr->poc_website() != nullptr)
138 info.poc_website = hdr->poc_website()->str();
139 if (hdr->poc_address_thoroughfare_number() != nullptr) {
140 info.poc_address_thoroughfare_number = hdr->poc_address_thoroughfare_number()->str();
141 }
142 if (hdr->poc_address_thoroughfare_name() != nullptr) {
143 info.poc_address_thoroughfare_name = hdr->poc_address_thoroughfare_name()->str();
144 }
145 if (hdr->poc_address_locality() != nullptr) {
146 info.poc_address_locality = hdr->poc_address_locality()->str();
147 }
148 if (hdr->poc_address_postcode() != nullptr) {
149 info.poc_address_postcode = hdr->poc_address_postcode()->str();
150 }
151 if (hdr->poc_address_country() != nullptr) {
152 info.poc_address_country = hdr->poc_address_country()->str();
153 }
154}
155
159std::uint64_t collect_attr_indices(const ::Header* hdr, std::vector<AttrIndexInfo>& out) {
160 const auto* ais = hdr->attribute_index();
161 if (ais == nullptr)
162 return 0;
163
164 out.reserve(ais->size());
165 for (const auto* ai : *ais) {
166 if (ai == nullptr)
167 continue;
168 AttrIndexInfo info{};
169 info.column_index = ai->index();
170 info.length = ai->length();
171 info.branching_factor = ai->branching_factor();
172 info.num_unique_items = ai->num_unique_items();
173 info.begin = 0; // filled once the layout is known
174 out.push_back(info);
175 }
176
177 std::sort(out.begin(), out.end(), [](const AttrIndexInfo& a, const AttrIndexInfo& b) {
178 return a.column_index < b.column_index;
179 });
180
181 // Two indexes claiming the same column makes the cumulative-offset walk
182 // ambiguous: there is no way to know which blob comes first.
183 for (std::size_t i = 1; i < out.size(); ++i) {
184 if (out[i].column_index == out[i - 1].column_index) {
185 throw Error(ErrorCode::AttributeIndexNotFound, "duplicate attribute index for column " +
186 std::to_string(out[i].column_index));
187 }
188 }
189
190 std::uint64_t total = 0;
191 for (const auto& ai : out) {
192 total = detail::checked_add(total, ai.length, "attr index total");
193 }
194 return total;
195}
196
197} // namespace
198
199HeaderView read_header(std::shared_ptr<RangeReader> reader) {
200 if (reader == nullptr) {
201 throw Error(ErrorCode::IoError, "read_header: null reader");
202 }
203 const std::uint64_t total_size = reader->total_size();
204
205 // Per-query buffering. 12944 = 2024 assumed header + the top 3 R-tree
206 // levels ((1 + 16 + 256) * 40), matching http_reader/mod.rs:80-98, so a
207 // remote open costs one range request rather than several.
208 BufferedRangeReader buffered(std::move(reader), 12944);
209
210 auto magic = buffered.read(0, kMagicBytesSize);
211 if (magic.size() < kMagicBytesSize || !check_magic_bytes(bytes_view(magic))) {
212 throw Error(ErrorCode::MissingMagicBytes, "not a FlatCityBuf file");
213 }
214
215 auto size_bytes = buffered.read(kMagicBytesSize, kHeaderSizeSize);
216 if (size_bytes.size() < kHeaderSizeSize) {
217 throw Error(ErrorCode::IllegalHeaderSize, "truncated before header size");
218 }
219 const std::uint32_t header_size = read_u32_le(size_bytes, 0);
220 if (header_size < kHeaderMinBufferSize || header_size > kHeaderMaxBufferSize) {
222 "illegal header size: " + std::to_string(header_size));
223 }
224
225 // The buffer handed to FlatBuffers MUST include the 4-byte size prefix:
226 // header_size is that prefix, not a bespoke length field.
227 //
228 // ALIGNMENT: the schema contains 8-byte-aligned structs (Transform,
229 // GeographicalExtent -- vectors of doubles). FlatBuffers aligns a
230 // size-prefixed buffer's contents relative to the buffer START, and
231 // std::vector's allocation is already at least 8-aligned, so copying the
232 // bytes into a fresh vector is sufficient.
233 const std::uint64_t want = kHeaderSizeSize + static_cast<std::uint64_t>(header_size);
234 auto raw_buf = buffered.read(kMagicBytesSize, want);
235 if (raw_buf.size() < want) {
236 throw Error(ErrorCode::IllegalHeaderSize, "truncated header");
237 }
238 std::vector<std::uint8_t> buf(kBodyAlignPad + raw_buf.size());
239 std::copy(raw_buf.begin(), raw_buf.end(), buf.begin() + kBodyAlignPad);
240
241 // FULL structural verification, alignment check included.
242 //
243 // This used to be disabled: flatbuffers 24.12.23's finish_size_prefixed
244 // laid out 8-byte-aligned structs relative to inconsistent bases, so
245 // Transform and GeographicalExtent ended up 60 bytes apart (60 % 8 == 4)
246 // and no buffer placement could align both. Bumping the Rust writer to
247 // flatbuffers 25.x fixed the layout, so the check now passes and is
248 // enforced.
249 flatbuffers::Verifier verifier(buf.data() + kBodyAlignPad, buf.size() - kBodyAlignPad);
250 if (!VerifySizePrefixedHeaderBuffer(verifier)) {
251 throw Error(ErrorCode::InvalidFlatbuffer, "header failed FlatBuffers verification");
252 }
253
254 HeaderView view;
255 view.buffer_ = std::make_shared<const std::vector<std::uint8_t>>(std::move(buf));
256
257 const ::Header* hdr = view.raw();
258 fill_metadata(hdr, view.info_);
259 fill_columns(hdr, view.info_);
260
261 const std::uint64_t attr_index_size = collect_attr_indices(hdr, view.attr_indices_);
262
263 view.layout_ = compute_layout(header_size, view.info_.features_count,
264 view.info_.index_node_size, attr_index_size);
265 validate_layout_against_size(view.layout_, total_size);
266
267 std::uint64_t cursor = view.layout_.attr_index_begin;
268 for (auto& ai : view.attr_indices_) {
269 ai.begin = cursor;
270 cursor = detail::checked_add(cursor, ai.length, "attr index begin");
271 }
272
273 return view;
274}
275
276} // namespace fcb
Caching decorator: over-fetches to min_req_size and serves subsequent reads inside the cached window ...
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.
Every failure the library reports is one of these.
Definition error.hpp:30
A parsed header that OWNS its backing bytes.
Definition header.hpp:108
std::uint64_t checked_add(std::uint64_t a, std::uint64_t b, const char *what="add")
Definition checked.hpp:16
span< const std::uint8_t > bytes_view
The workhorse alias: a read-only view over bytes.
Definition span.hpp:43
constexpr std::size_t kHeaderMaxBufferSize
Definition layout.hpp:13
constexpr std::size_t kHeaderSizeSize
Definition layout.hpp:11
HeaderView read_header(std::shared_ptr< RangeReader > reader)
Read and validate the file preamble and header.
Definition header.cpp:199
FileLayout compute_layout(std::uint32_t header_size, std::uint64_t features_count, std::uint16_t index_node_size, std::uint64_t attr_index_size)
Throws fcb::Error{IllegalHeaderSize} when header_size is out of range or any size arithmetic overflow...
Definition layout.cpp:49
void validate_layout_against_size(const FileLayout &l, std::uint64_t total_size)
Throws unless the computed sections fit inside the resource.
Definition layout.cpp:70
bool check_magic_bytes(bytes_view b)
Mirrors fcb_core::check_magic_bytes (src/rust/fcb_core/src/lib.rs:56-58).
Definition layout.cpp:14
constexpr std::size_t kMagicBytesSize
Definition layout.hpp:10
static constexpr std::size_t kBodyAlignPad
No padding needed.
Definition header.cpp:20
RangeReader & reader
Definition stree.cpp:162
std::uint16_t index_node_size
Definition header.hpp:46
std::uint64_t features_count
Definition header.hpp:45
std::uint64_t attr_index_begin
Definition layout.hpp:36
static const ::Header * get(const HeaderView &h)
Definition header.cpp:30