use num_enum::TryFromPrimitive;
use crate::{MltError, MltResult};
#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
#[repr(u8)]
pub(crate) enum DataType02 {
Id = 0x00,
LongId = 0x01,
Bool = 0x02,
I8 = 0x03,
U8 = 0x04,
I32 = 0x05,
U32 = 0x06,
I64 = 0x07,
U64 = 0x08,
F32 = 0x09,
F64 = 0x0A,
}
impl DataType02 {
#[must_use]
pub(crate) fn has_name(self) -> bool {
!matches!(self, Self::Id | Self::LongId)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Presence02 {
AllPresent,
Inline,
Shared(u8),
}
impl Presence02 {
const ALL_PRESENT: u8 = 0b0000_0000;
const INLINE: u8 = 0b0001_0000;
const SHARED_BASE: u8 = 0b0010_0000;
#[must_use]
pub(crate) fn parse(nibble: u8, shared_count: u8) -> Option<Self> {
match nibble {
Self::ALL_PRESENT => Some(Self::AllPresent),
Self::INLINE => Some(Self::Inline),
_ => {
let index = (nibble - Self::SHARED_BASE) >> 4;
(index < shared_count).then_some(Self::Shared(index))
}
}
}
#[must_use]
pub(crate) fn is_optional(self) -> bool {
!matches!(self, Self::AllPresent)
}
#[must_use]
fn to_nibble(self) -> u8 {
match self {
Self::AllPresent => Self::ALL_PRESENT,
Self::Inline => Self::INLINE,
Self::Shared(index) => {
debug_assert!(index < LayerLayout::MAX_SHARED_PRESENCE);
Self::SHARED_BASE + (index << 4)
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ColumnType02 {
pub(crate) presence: Presence02,
pub(crate) data: DataType02,
}
impl ColumnType02 {
const PRESENCE_MASK: u8 = 0b1111_0000;
const DATA_TYPE_MASK: u8 = 0b0000_1111;
#[must_use]
pub(crate) fn new(presence: Presence02, data: DataType02) -> Self {
Self { presence, data }
}
#[must_use]
pub(crate) fn fields(byte: u8) -> (u8, u8) {
(byte & Self::PRESENCE_MASK, byte & Self::DATA_TYPE_MASK)
}
pub(crate) fn parse(byte: u8, shared_count: u8) -> MltResult<Self> {
let err = || MltError::ParsingColumnType(byte);
let (presence, data) = Self::fields(byte);
let presence = Presence02::parse(presence, shared_count).ok_or_else(err)?;
let data = DataType02::try_from(data).map_err(|_| err())?;
Ok(Self { presence, data })
}
#[must_use]
pub(crate) fn to_byte(self) -> u8 {
self.presence.to_nibble() | self.data as u8
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
#[repr(u8)]
pub(crate) enum GeoLayout {
Points = 0x00,
PointsDict = 0x01,
MultiPoints = 0x02,
MultiPointsDict = 0x03,
Lines = 0x04,
LinesDict = 0x05,
MultiLines = 0x06,
MultiLinesDict = 0x07,
Polygons = 0x08,
PolygonsDict = 0x09,
MultiPolygons = 0x0A,
MultiPolygonsDict = 0x0B,
TessPolygons = 0x0C,
TessPolygonsWithOutlines = 0x0D,
}
impl GeoLayout {
pub(crate) fn from_streams(geo: bool, part: bool, ring: bool) -> MltResult<Self> {
Ok(match (geo, part, ring) {
(false, false, false) => Self::Points,
(true, false, false) => Self::MultiPoints,
(false, true, false) => Self::Lines,
(true, true, false) => Self::MultiLines,
(false, true, true) => Self::Polygons,
(true, true, true) => Self::MultiPolygons,
(_, false, true) => Err(MltError::NotImplemented(
"v2 geometry: ring lengths without part lengths",
))?,
})
}
#[must_use]
pub(crate) fn has_geo_lengths(self) -> bool {
matches!(
self,
Self::MultiPoints
| Self::MultiPointsDict
| Self::MultiLines
| Self::MultiLinesDict
| Self::MultiPolygons
| Self::MultiPolygonsDict
| Self::TessPolygonsWithOutlines
)
}
#[must_use]
pub(crate) fn has_part_lengths(self) -> bool {
matches!(
self,
Self::Lines
| Self::LinesDict
| Self::MultiLines
| Self::MultiLinesDict
| Self::Polygons
| Self::PolygonsDict
| Self::MultiPolygons
| Self::MultiPolygonsDict
| Self::TessPolygonsWithOutlines
)
}
#[must_use]
pub(crate) fn has_ring_lengths(self) -> bool {
matches!(
self,
Self::Polygons
| Self::PolygonsDict
| Self::MultiPolygons
| Self::MultiPolygonsDict
| Self::TessPolygonsWithOutlines
)
}
#[must_use]
pub(crate) fn is_dict(self) -> bool {
matches!(
self,
Self::PointsDict
| Self::MultiPointsDict
| Self::LinesDict
| Self::MultiLinesDict
| Self::PolygonsDict
| Self::MultiPolygonsDict
)
}
#[must_use]
pub(crate) fn is_tess(self) -> bool {
matches!(self, Self::TessPolygons | Self::TessPolygonsWithOutlines)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct LayerLayout {
pub(crate) shared_presence: u8,
pub(crate) geometry: GeoLayout,
}
impl LayerLayout {
const RESERVED_MASK: u8 = 0b1000_0000;
const SHARED_PRESENCE_MASK: u8 = 0b0111_0000;
const GEO_LAYOUT_MASK: u8 = 0b0000_1111;
pub(crate) const MAX_SHARED_PRESENCE: u8 = Self::SHARED_PRESENCE_MASK >> 4;
#[must_use]
pub(crate) fn new(geometry: GeoLayout, shared_presence: u8) -> Self {
debug_assert!(shared_presence <= Self::MAX_SHARED_PRESENCE);
Self {
shared_presence,
geometry,
}
}
#[must_use]
pub(crate) fn fields(byte: u8) -> (u8, u8, u8) {
(
byte & Self::RESERVED_MASK,
(byte & Self::SHARED_PRESENCE_MASK) >> 4,
byte & Self::GEO_LAYOUT_MASK,
)
}
pub(crate) fn parse(byte: u8) -> MltResult<Self> {
let (reserved, shared_presence, geometry) = Self::fields(byte);
if reserved != 0 {
return Err(MltError::ParsingLayerLayout(byte));
}
let geometry =
GeoLayout::try_from(geometry).map_err(|_| MltError::ParsingGeoLayout(geometry))?;
Ok(Self {
shared_presence,
geometry,
})
}
#[must_use]
pub(crate) fn to_byte(self) -> u8 {
debug_assert!(self.shared_presence <= Self::MAX_SHARED_PRESENCE);
(self.shared_presence << 4) | self.geometry as u8
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
const ALL_SHARED: u8 = LayerLayout::MAX_SHARED_PRESENCE;
#[rstest]
#[case::id(0b0000_0000, Presence02::AllPresent, DataType02::Id)]
#[case::opt_id(0b0001_0000, Presence02::Inline, DataType02::Id)]
#[case::i32(0b0000_0101, Presence02::AllPresent, DataType02::I32)]
#[case::opt_f64(0b0001_1010, Presence02::Inline, DataType02::F64)]
#[case::first_shared(0b0010_0101, Presence02::Shared(0), DataType02::I32)]
#[case::last_shared(0b1000_1010, Presence02::Shared(6), DataType02::F64)]
fn column_type_byte_roundtrip(
#[case] byte: u8,
#[case] presence: Presence02,
#[case] data: DataType02,
) {
let typ = ColumnType02::parse(byte, ALL_SHARED).unwrap();
assert_eq!(typ, ColumnType02::new(presence, data));
assert_eq!(typ.to_byte(), byte);
}
#[rstest]
#[case::reserved_data_type(0b0000_1111, ALL_SHARED)]
#[case::unassigned_data_type(0b0000_1011, ALL_SHARED)]
#[case::reserved_presence(0b1001_0101, ALL_SHARED)]
#[case::reserved_presence_top(0b1111_0101, ALL_SHARED)]
#[case::shared_ref_without_shared_columns(0b0010_0101, 0)]
#[case::shared_ref_past_declared_count(0b0100_0101, 1)]
fn column_type_byte_rejects_unassigned(#[case] byte: u8, #[case] shared_count: u8) {
let err = ColumnType02::parse(byte, shared_count).unwrap_err();
assert!(matches!(err, MltError::ParsingColumnType(b) if b == byte));
}
#[rstest]
#[case::points(0b0000_0000, 0, GeoLayout::Points)]
#[case::multi_polygons(0b0000_1010, 0, GeoLayout::MultiPolygons)]
#[case::one_shared_presence(0b0001_0100, 1, GeoLayout::Lines)]
#[case::max_shared_presence(0b0111_0000, 7, GeoLayout::Points)]
fn layer_layout_byte_roundtrip(
#[case] byte: u8,
#[case] shared_presence: u8,
#[case] geometry: GeoLayout,
) {
let layout = LayerLayout::parse(byte).unwrap();
assert_eq!(layout, LayerLayout::new(geometry, shared_presence));
assert_eq!(layout.to_byte(), byte);
}
#[rstest]
#[case::unassigned_geo_layout(0b0000_1110)]
#[case::unassigned_geo_layout_with_shared(0b0010_1111)]
fn layer_layout_byte_rejects_unassigned_geo_layout(#[case] byte: u8) {
let err = LayerLayout::parse(byte).unwrap_err();
assert!(
matches!(err, MltError::ParsingGeoLayout(b) if b == byte & LayerLayout::GEO_LAYOUT_MASK)
);
}
#[rstest]
#[case::reserved_bit(0b1000_0000)]
#[case::reserved_bit_with_shared(0b1111_0000)]
fn layer_layout_byte_rejects_reserved_bit(#[case] byte: u8) {
let err = LayerLayout::parse(byte).unwrap_err();
assert!(matches!(err, MltError::ParsingLayerLayout(b) if b == byte));
}
}