#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ByteOrder {
Big,
Little,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Envelope {
None,
Xy([f64; 4]),
Xyz([f64; 6]),
Xym([f64; 6]),
Xyzm([f64; 8]),
}
impl Envelope {
pub fn indicator(&self) -> u8 {
match self {
Envelope::None => 0,
Envelope::Xy(_) => 1,
Envelope::Xyz(_) => 2,
Envelope::Xym(_) => 3,
Envelope::Xyzm(_) => 4,
}
}
pub fn values(&self) -> &[f64] {
match self {
Envelope::None => &[],
Envelope::Xy(v) => v,
Envelope::Xyz(v) | Envelope::Xym(v) => v,
Envelope::Xyzm(v) => v,
}
}
pub fn xy_bounds(&self) -> Option<(f64, f64, f64, f64)> {
match *self.values() {
[minx, maxx, miny, maxy, ..] => Some((minx, maxx, miny, maxy)),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GpbHeader {
pub srs_id: i32,
pub envelope: Envelope,
pub empty: bool,
pub extended: bool,
pub byte_order: ByteOrder,
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum GpbError {
#[error("GPB blob truncated: need {expected} bytes, have {actual}")]
Truncated {
expected: usize,
actual: usize,
},
#[error("bad GPB magic: {0:#04x} {1:#04x}")]
BadMagic(u8, u8),
#[error("unsupported GPB version {0} (only version 0 is defined)")]
UnsupportedVersion(u8),
#[error("invalid envelope contents indicator {0}")]
InvalidEnvelopeIndicator(u8),
}
const MAGIC: [u8; 2] = [0x47, 0x50]; const HEADER_BASE_LEN: usize = 8;
pub fn body_offset(blob: &[u8]) -> Result<usize, GpbError> {
let &[m0, m1, version, flags, ..] = blob else {
return Err(GpbError::Truncated {
expected: HEADER_BASE_LEN,
actual: blob.len(),
});
};
if [m0, m1] != MAGIC {
return Err(GpbError::BadMagic(m0, m1));
}
if version != 0 {
return Err(GpbError::UnsupportedVersion(version));
}
let doubles = match (flags >> 1) & 0b111 {
0 => 0,
1 => 4,
2 | 3 => 6,
4 => 8,
other => return Err(GpbError::InvalidEnvelopeIndicator(other)),
};
let offset = HEADER_BASE_LEN + doubles * 8;
if blob.len() < offset {
return Err(GpbError::Truncated {
expected: offset,
actual: blob.len(),
});
}
Ok(offset)
}
pub fn parse_header(blob: &[u8]) -> Result<(GpbHeader, usize), GpbError> {
let &[m0, m1, version, flags, s0, s1, s2, s3, ref rest @ ..] = blob else {
return Err(GpbError::Truncated {
expected: HEADER_BASE_LEN,
actual: blob.len(),
});
};
if [m0, m1] != MAGIC {
return Err(GpbError::BadMagic(m0, m1));
}
if version != 0 {
return Err(GpbError::UnsupportedVersion(version));
}
let byte_order = if flags & 0b1 == 1 {
ByteOrder::Little
} else {
ByteOrder::Big
};
let indicator = (flags >> 1) & 0b111;
let empty = flags & 0b1_0000 != 0;
let extended = flags & 0b10_0000 != 0;
let srs_bytes = [s0, s1, s2, s3];
let srs_id = match byte_order {
ByteOrder::Big => i32::from_be_bytes(srs_bytes),
ByteOrder::Little => i32::from_le_bytes(srs_bytes),
};
let envelope = match indicator {
0 => Envelope::None,
1 => Envelope::Xy(read_doubles(rest, byte_order, blob.len())?),
2 => Envelope::Xyz(read_doubles(rest, byte_order, blob.len())?),
3 => Envelope::Xym(read_doubles(rest, byte_order, blob.len())?),
4 => Envelope::Xyzm(read_doubles(rest, byte_order, blob.len())?),
_ => return Err(GpbError::InvalidEnvelopeIndicator(indicator)),
};
let body_offset = HEADER_BASE_LEN + envelope.values().len() * 8;
Ok((
GpbHeader {
srs_id,
envelope,
empty,
extended,
byte_order,
},
body_offset,
))
}
fn read_doubles<const N: usize>(
rest: &[u8],
byte_order: ByteOrder,
total_len: usize,
) -> Result<[f64; N], GpbError> {
let Some(region) = rest.get(..N * 8) else {
return Err(GpbError::Truncated {
expected: HEADER_BASE_LEN + N * 8,
actual: total_len,
});
};
let mut vals = [0f64; N];
for (slot, bytes) in vals.iter_mut().zip(region.chunks_exact(8)) {
let &[b0, b1, b2, b3, b4, b5, b6, b7] = bytes else {
continue;
};
let word = [b0, b1, b2, b3, b4, b5, b6, b7];
*slot = match byte_order {
ByteOrder::Big => f64::from_be_bytes(word),
ByteOrder::Little => f64::from_le_bytes(word),
};
}
Ok(vals)
}
pub fn encode_header(srs_id: i32, envelope: &Envelope, empty: bool, extended: bool) -> Vec<u8> {
let vals = envelope.values();
let mut out = Vec::with_capacity(HEADER_BASE_LEN + vals.len() * 8);
out.extend_from_slice(&MAGIC);
out.push(0); let mut flags = 0b1u8; flags |= envelope.indicator() << 1;
if empty {
flags |= 0b1_0000;
}
if extended {
flags |= 0b10_0000;
}
out.push(flags);
out.extend_from_slice(&srs_id.to_le_bytes());
for v in vals {
out.extend_from_slice(&v.to_le_bytes());
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn roundtrip(srs_id: i32, envelope: Envelope, empty: bool, extended: bool) {
let buf = encode_header(srs_id, &envelope, empty, extended);
let (h, off) = parse_header(&buf).unwrap();
assert_eq!(off, buf.len());
assert_eq!(h.srs_id, srs_id);
assert_eq!(h.envelope, envelope);
assert_eq!(h.empty, empty);
assert_eq!(h.extended, extended);
assert_eq!(h.byte_order, ByteOrder::Little);
}
#[test]
fn roundtrips() {
roundtrip(4326, Envelope::None, false, false);
roundtrip(-1, Envelope::Xy([1.0, 2.0, 3.0, 4.0]), false, false);
roundtrip(
0,
Envelope::Xyz([1.0, 2.0, 3.0, 4.0, -5.0, 5.0]),
false,
false,
);
roundtrip(
3857,
Envelope::Xym([1.0, 2.0, 3.0, 4.0, 0.0, 9.0]),
false,
true,
);
roundtrip(
i32::MIN,
Envelope::Xyzm([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]),
true,
false,
);
}
#[test]
fn parses_big_endian() {
let mut buf = vec![0x47, 0x50, 0x00, 0b0000_0010];
buf.extend_from_slice(&4326i32.to_be_bytes());
for v in [1.0f64, 2.0, 3.0, 4.0] {
buf.extend_from_slice(&v.to_be_bytes());
}
let (h, off) = parse_header(&buf).unwrap();
assert_eq!(h.byte_order, ByteOrder::Big);
assert_eq!(h.srs_id, 4326);
assert_eq!(h.envelope, Envelope::Xy([1.0, 2.0, 3.0, 4.0]));
assert_eq!(off, 40);
}
#[test]
fn rejects_garbage() {
assert_eq!(
parse_header(b"GX\x00\x01aaaa"),
Err(GpbError::BadMagic(0x47, 0x58))
);
assert_eq!(
parse_header(b"GP\x01\x01aaaa"),
Err(GpbError::UnsupportedVersion(1))
);
assert_eq!(
parse_header(&[0x47, 0x50, 0x00, 5 << 1, 0, 0, 0, 0]),
Err(GpbError::InvalidEnvelopeIndicator(5))
);
assert!(matches!(
parse_header(&[0x47, 0x50, 0x00, 0b0000_0011, 0, 0, 0, 0, 1, 2, 3]),
Err(GpbError::Truncated {
expected: 40,
actual: 11
})
));
assert!(matches!(
parse_header(b"GP"),
Err(GpbError::Truncated { .. })
));
}
#[test]
fn reserved_bits_tolerated_on_read() {
let mut buf = encode_header(4326, &Envelope::None, false, false);
buf[3] |= 0b1100_0000;
parse_header(&buf).unwrap();
}
}
#[cfg(test)]
mod body_offset_tests {
use super::*;
fn blob(indicator: u8, doubles: usize) -> Vec<u8> {
let mut out = vec![b'G', b'P', 0, 0b1 | (indicator << 1), 0, 0, 0, 0];
out.extend(std::iter::repeat_n(0u8, doubles * 8));
out.push(0x01);
out
}
#[test]
fn agrees_with_parse_header_for_every_indicator() {
for (indicator, doubles) in [(0u8, 0usize), (1, 4), (2, 6), (3, 6), (4, 8)] {
let blob = blob(indicator, doubles);
let (_, expected) = parse_header(&blob).expect("a valid blob parses");
assert_eq!(
body_offset(&blob),
Ok(expected),
"indicator {indicator} disagrees"
);
}
}
#[test]
fn rejects_what_parse_header_rejects() {
body_offset(&[b'X', b'P', 0, 0b1, 0, 0, 0, 0, 1]).unwrap_err();
body_offset(&[b'G', b'P', 9, 0b1, 0, 0, 0, 0, 1]).unwrap_err();
body_offset(&[b'G', b'P', 0, 0b1 | (5 << 1), 0, 0, 0, 0, 1]).unwrap_err();
body_offset(&[b'G', b'P', 0]).unwrap_err();
body_offset(&[b'G', b'P', 0, 0b1 | (1 << 1), 0, 0, 0, 0]).unwrap_err();
}
}