use crate::error::{Error, Result};
const MAGIC: [u8; 2] = [0x47, 0x50];
const FLAG_BYTE_ORDER_LE: u8 = 0b0000_0001;
const FLAG_EMPTY: u8 = 0b0001_0000;
const FLAG_EXTENDED: u8 = 0b0010_0000;
const FLAG_RESERVED: u8 = 0b1100_0000;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Envelope {
pub min_x: f64,
pub max_x: f64,
pub min_y: f64,
pub max_y: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct GpbHeader {
pub srid: i32,
pub envelope: Option<Envelope>,
pub empty: bool,
pub wkb_offset: usize,
}
impl GpbHeader {
pub fn parse(bytes: &[u8]) -> Result<GpbHeader> {
if bytes.len() < 8 {
return Err(Error::InvalidGpb("blob shorter than the 8-byte header"));
}
if bytes[0..2] != MAGIC {
return Err(Error::InvalidGpb("missing \"GP\" magic"));
}
if bytes[2] != 0 {
return Err(Error::InvalidGpb("unsupported version (expected 0)"));
}
let flags = bytes[3];
if flags & FLAG_EXTENDED != 0 {
return Err(Error::InvalidGpb(
"ExtendedGeoPackageBinary is not supported",
));
}
if flags & FLAG_RESERVED != 0 {
return Err(Error::InvalidGpb("reserved flag bits are set"));
}
let little_endian = flags & FLAG_BYTE_ORDER_LE != 0;
let envelope_indicator = (flags >> 1) & 0b111;
let envelope_doubles = match envelope_indicator {
0 => 0,
1 => 4, 2 | 3 => 6, 4 => 8, _ => return Err(Error::InvalidGpb("invalid envelope indicator (5-7)")),
};
let wkb_offset = 8 + envelope_doubles * 8;
if bytes.len() < wkb_offset {
return Err(Error::InvalidGpb("blob truncated inside the envelope"));
}
let srid_bytes: [u8; 4] = bytes[4..8].try_into().expect("length checked");
let srid = if little_endian {
i32::from_le_bytes(srid_bytes)
} else {
i32::from_be_bytes(srid_bytes)
};
let envelope = if envelope_doubles >= 4 {
let mut d = [0.0f64; 4];
for (i, v) in d.iter_mut().enumerate() {
let start = 8 + i * 8;
let raw: [u8; 8] = bytes[start..start + 8].try_into().expect("length checked");
*v = if little_endian {
f64::from_le_bytes(raw)
} else {
f64::from_be_bytes(raw)
};
}
Some(Envelope {
min_x: d[0],
max_x: d[1],
min_y: d[2],
max_y: d[3],
})
} else {
None
};
Ok(GpbHeader {
srid,
envelope,
empty: flags & FLAG_EMPTY != 0,
wkb_offset,
})
}
}
pub fn write_gpb(wkb: &[u8], srid: i32, envelope: Option<Envelope>, empty: bool) -> Vec<u8> {
let mut flags = FLAG_BYTE_ORDER_LE;
if envelope.is_some() {
flags |= 1 << 1; }
if empty {
flags |= FLAG_EMPTY;
}
let mut out = Vec::with_capacity(8 + if envelope.is_some() { 32 } else { 0 } + wkb.len());
out.extend_from_slice(&MAGIC);
out.push(0); out.push(flags);
out.extend_from_slice(&srid.to_le_bytes());
if let Some(e) = envelope {
for v in [e.min_x, e.max_x, e.min_y, e.max_y] {
out.extend_from_slice(&v.to_le_bytes());
}
}
out.extend_from_slice(wkb);
out
}
pub fn is_gpb(bytes: &[u8]) -> bool {
bytes.len() >= 2 && bytes[0..2] == MAGIC
}
#[cfg(test)]
mod tests {
use super::*;
fn point_wkb() -> Vec<u8> {
let mut wkb = vec![0x01, 0x01, 0x00, 0x00, 0x00];
wkb.extend_from_slice(&1.0f64.to_le_bytes());
wkb.extend_from_slice(&2.0f64.to_le_bytes());
wkb
}
#[test]
fn parse_header_without_envelope_byte_literal() {
let mut blob = vec![0x47, 0x50, 0x00, 0x01, 0xE6, 0x10, 0x00, 0x00];
blob.extend_from_slice(&point_wkb());
let h = GpbHeader::parse(&blob).unwrap();
assert_eq!(h.srid, 4326);
assert_eq!(h.envelope, None);
assert!(!h.empty);
assert_eq!(h.wkb_offset, 8);
}
#[test]
fn parse_envelope_value_order_is_minx_maxx_miny_maxy() {
let mut blob = vec![0x47, 0x50, 0x00, 0x03, 0xE6, 0x10, 0x00, 0x00];
for v in [1.0f64, 2.0, 3.0, 4.0] {
blob.extend_from_slice(&v.to_le_bytes());
}
blob.extend_from_slice(&point_wkb());
let h = GpbHeader::parse(&blob).unwrap();
assert_eq!(
h.envelope,
Some(Envelope {
min_x: 1.0,
max_x: 2.0,
min_y: 3.0,
max_y: 4.0
})
);
assert_eq!(h.wkb_offset, 40);
}
#[test]
fn parse_big_endian_header() {
let mut blob = vec![0x47, 0x50, 0x00, 0x02];
blob.extend_from_slice(&4326i32.to_be_bytes());
for v in [1.0f64, 2.0, 3.0, 4.0] {
blob.extend_from_slice(&v.to_be_bytes());
}
blob.extend_from_slice(&point_wkb());
let h = GpbHeader::parse(&blob).unwrap();
assert_eq!(h.srid, 4326);
assert_eq!(h.envelope.unwrap().max_y, 4.0);
}
#[test]
fn parse_all_envelope_indicators() {
for (indicator, doubles) in [(0usize, 0usize), (1, 4), (2, 6), (3, 6), (4, 8)] {
let flags = 0x01 | (indicator as u8) << 1;
let mut blob = vec![0x47, 0x50, 0x00, flags, 0x00, 0x00, 0x00, 0x00];
for i in 0..doubles {
blob.extend_from_slice(&(i as f64).to_le_bytes());
}
blob.extend_from_slice(&point_wkb());
let h = GpbHeader::parse(&blob).unwrap();
assert_eq!(h.wkb_offset, 8 + doubles * 8, "indicator {indicator}");
assert_eq!(h.envelope.is_some(), doubles >= 4);
}
}
#[test]
fn parse_empty_flag() {
let blob = vec![0x47, 0x50, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00];
let h = GpbHeader::parse(&blob).unwrap();
assert!(h.empty);
}
#[test]
fn rejects_bad_magic_version_and_flags() {
let ok = vec![0x47, 0x50, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00];
let cases: Vec<(usize, u8)> = vec![
(0, 0x00), (2, 0x01), (3, 0x21), (3, 0x41), (3, 0x0B), ];
for (pos, byte) in cases {
let mut blob = ok.clone();
blob[pos] = byte;
assert!(GpbHeader::parse(&blob).is_err(), "byte {byte:#x} at {pos}");
}
}
#[test]
fn truncation_at_every_offset_errors_never_panics() {
let mut blob = vec![0x47, 0x50, 0x00, 0x03, 0xE6, 0x10, 0x00, 0x00];
for v in [1.0f64, 2.0, 3.0, 4.0] {
blob.extend_from_slice(&v.to_le_bytes());
}
let full = GpbHeader::parse(&blob);
assert!(full.is_ok());
for len in 0..blob.len() {
assert!(GpbHeader::parse(&blob[..len]).is_err(), "prefix {len}");
}
}
#[test]
fn write_parse_roundtrip() {
let env = Envelope {
min_x: -1.5,
max_x: 2.5,
min_y: -3.5,
max_y: 4.5,
};
for (envelope, empty) in [(None, false), (Some(env), false), (None, true)] {
let blob = write_gpb(&point_wkb(), 3857, envelope, empty);
let h = GpbHeader::parse(&blob).unwrap();
assert_eq!(h.srid, 3857);
assert_eq!(h.envelope, envelope);
assert_eq!(h.empty, empty);
assert_eq!(&blob[h.wkb_offset..], &point_wkb()[..]);
}
}
#[test]
fn is_gpb_detects_magic() {
assert!(is_gpb(&[0x47, 0x50, 0x00, 0x01]));
assert!(!is_gpb(&[0x01, 0x01]));
assert!(!is_gpb(&[0x47]));
}
}