///|
pub fn validate_magic(data : Bytes) -> Unit raise FgbError {
require_range(data, 0, 8)
let expected = b"\x66\x67\x62\x03\x66\x67\x62"
for i in 0..<7 {
if data[i] != expected[i] {
raise InvalidData(i, "invalid FlatGeobuf magic or major version")
}
}
}
///|
fn sized_payload(
data : Bytes,
at : Int32,
limit : Int32,
) -> Bytes raise FgbError {
let size = size32(data, at)
enforce(size, limit, "size-prefixed buffer")
if size < 4 {
raise InvalidData(at, "size-prefixed buffer is too small")
}
slice_bytes(data, checked_add(at, 4), size)
}
///|
fn read_column(
data : Bytes,
at : Int32,
limits : Limits,
) -> Column raise FgbError {
let t = TableView::open(data, at)
let name = t.text(0, limits)
if name.is_empty() {
raise InvalidData(at, "column name is required")
}
let code = t.byte(1)
if code > 14 {
raise Unsupported("column type \{code}")
}
{
name,
type_code: code,
title: t.text(2, limits),
description: t.text(3, limits),
width: t.int(4, fallback=-1),
precision: t.int(5, fallback=-1),
scale: t.int(6, fallback=-1),
nullable: t.byte(7, fallback=1) != 0,
unique: t.byte(8) != 0,
primary_key: t.byte(9) != 0,
metadata: t.text(10, limits),
}
}
///|
fn read_crs(data : Bytes, at : Int32, limits : Limits) -> Crs raise FgbError {
let t = TableView::open(data, at)
{
org: t.text(0, limits),
code: t.int(1),
name: t.text(2, limits),
description: t.text(3, limits),
wkt: t.text(4, limits),
code_string: t.text(5, limits),
}
}
///|
pub fn decode_header(
data : Bytes,
limits~ : Limits = Limits::default(),
) -> Header raise FgbError {
limits.validate()
let payload = sized_payload(data, 0, limits.max_header_bytes)
if payload.length() + 4 != data.length() {
raise InvalidData(0, "trailing data after header")
}
let t = TableView::root(payload)
for field in 3..<7 {
if t.byte(field) != 0 {
raise Unsupported("Z/M/T/TM coordinates")
}
}
let columns : Array[Column] = []
let (start, count) = t.vector(7, 4, limits.max_columns)
let names : Map[String, Bool] = Map([])
for i in 0.. 0 && features_count == 0) {
raise InvalidData(t.position, "index needs count > 0 and node size >= 2")
}
let (envelope_at, envelope_count) = t.vector(1, 8, 8)
let envelope = if envelope_count == 0 {
None
} else if envelope_count == 4 {
let bounds : Bounds = {
min_x: f64(payload, envelope_at),
min_y: f64(payload, envelope_at + 8),
max_x: f64(payload, envelope_at + 16),
max_y: f64(payload, envelope_at + 24),
}
bounds.validate()
Some(bounds)
} else {
raise Unsupported("only 2D envelopes are supported")
}
let crs_at = t.reference(10)
{
name: t.text(0, limits),
geometry_kind: kind_from_code(t.byte(2)),
columns,
features_count,
index_node_size: node_size,
envelope,
crs: if crs_at == 0 {
None
} else {
Some(read_crs(payload, crs_at, limits))
},
title: t.text(11, limits),
description: t.text(12, limits),
metadata: t.text(13, limits),
}
}
///|
fn write_column(b : FbBuilder, column : Column) -> Int32 {
let table = b.table(11)
b.text(table, 0, column.name)
b.scalar(table, 1, column.type_code)
if !column.title.is_empty() {
b.text(table, 2, column.title)
}
if !column.description.is_empty() {
b.text(table, 3, column.description)
}
b.scalar(table, 4, column.width)
b.scalar(table, 5, column.precision)
b.scalar(table, 6, column.scale)
b.scalar(table, 7, if column.nullable { 1 } else { 0 })
b.scalar(table, 8, if column.unique { 1 } else { 0 })
b.scalar(table, 9, if column.primary_key { 1 } else { 0 })
if !column.metadata.is_empty() {
b.text(table, 10, column.metadata)
}
table
}
///|
fn write_crs(b : FbBuilder, crs : Crs) -> Int32 {
let table = b.table(6)
b.text(table, 0, crs.org)
b.scalar(table, 1, crs.code)
b.text(table, 2, crs.name)
b.text(table, 3, crs.description)
b.text(table, 4, crs.wkt)
b.text(table, 5, crs.code_string)
table
}
///|
pub fn encode_header(
header : Header,
limits~ : Limits = Limits::default(),
) -> Bytes raise FgbError {
limits.validate()
validate_header(header, limits~)
let b = FbBuilder::new()
let table = b.table(14)
b.text(table, 0, header.name)
match header.envelope {
Some(bounds) => {
let at = b.vector(4, 8)
b.writer.double(bounds.min_x)
b.writer.double(bounds.min_y)
b.writer.double(bounds.max_x)
b.writer.double(bounds.max_y)
b.reference(table, 1, at)
}
None => ()
}
b.scalar(table, 2, header.geometry_kind.code())
if !header.columns.is_empty() {
let vector = b.vector(header.columns.length(), 4)
b.writer.zeros(header.columns.length() * 4)
for i in 0.. b.reference(table, 10, write_crs(b, crs))
None => ()
}
b.text(table, 11, header.title)
b.text(table, 12, header.description)
b.text(table, 13, header.metadata)
let bytes = b.finish(table)
enforce(bytes.length() - 4, limits.max_header_bytes, "header bytes")
bytes
}
///|
pub fn validate_header(
header : Header,
limits~ : Limits = Limits::default(),
) -> Unit raise FgbError {
if header.features_count < 0 ||
header.index_node_size < 0 ||
header.index_node_size > 65535 ||
header.index_node_size == 1 {
raise InvalidArgument("invalid feature count or index node size")
}
enforce(header.features_count, limits.max_features, "feature count")
enforce(header.columns.length(), limits.max_columns, "column count")
let seen : Map[String, Bool] = Map([])
for column in header.columns {
if column.name.is_empty() || seen.contains(column.name) {
raise InvalidArgument("empty or duplicate column name")
}
if column.type_code < 0 || column.type_code > 14 {
raise Unsupported("column type \{column.type_code}")
}
seen[column.name] = true
enforce(
@utf8.encode(column.name).length(),
limits.max_string_bytes,
"column name",
)
}
match header.envelope {
Some(bounds) => bounds.validate()
None => ()
}
if header.index_node_size > 0 && header.features_count == 0 {
raise InvalidArgument("indexed files require a known non-zero count")
}
}