fix(mysql): parse geometry WKB to WKT for display and editing
This commit is contained in:
parent
3695042c8e
commit
63bb6a21ca
|
|
@ -801,6 +801,13 @@ pub fn format_grid_sql_literal(
|
|||
if text.is_empty() {
|
||||
return if database_type == Some(DatabaseType::SqlServer) { "N''" } else { "''" }.to_string();
|
||||
}
|
||||
// MySQL geometry columns: wrap WKT text with ST_GeomFromText()
|
||||
if is_mysql_geometry_literal_database(database_type)
|
||||
&& column_info.map(|column| is_geometry_column_type(&column.data_type)).unwrap_or(false)
|
||||
{
|
||||
let escaped = text.replace('\\', "\\\\").replace('\'', "''");
|
||||
return format!("ST_GeomFromText('{}')", escaped);
|
||||
}
|
||||
let literal_text = if database_type == Some(DatabaseType::Tdengine) {
|
||||
format_tdengine_timestamp_literal_text(&text)
|
||||
} else if is_mysql_datetime_literal_database(database_type)
|
||||
|
|
@ -832,6 +839,35 @@ fn is_bit_column_type(data_type: &str) -> bool {
|
|||
lower.split(|ch: char| !ch.is_ascii_alphanumeric()).any(|token| token == "bit")
|
||||
}
|
||||
|
||||
fn is_mysql_geometry_literal_database(database_type: Option<DatabaseType>) -> bool {
|
||||
matches!(
|
||||
database_type,
|
||||
Some(
|
||||
DatabaseType::Mysql
|
||||
| DatabaseType::Doris
|
||||
| DatabaseType::StarRocks
|
||||
| DatabaseType::Goldendb
|
||||
| DatabaseType::Sundb
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fn is_geometry_column_type(data_type: &str) -> bool {
|
||||
let lower = data_type.to_ascii_lowercase();
|
||||
let base = lower.split('(').next().unwrap_or(&lower).trim();
|
||||
matches!(
|
||||
base,
|
||||
"geometry"
|
||||
| "point"
|
||||
| "linestring"
|
||||
| "polygon"
|
||||
| "multipoint"
|
||||
| "multilinestring"
|
||||
| "multipolygon"
|
||||
| "geometrycollection"
|
||||
)
|
||||
}
|
||||
|
||||
fn manticore_typed_attribute_value(text: &str, column_info: Option<&DataGridColumnInfo>) -> Option<Value> {
|
||||
let data_type = column_info?.data_type.to_ascii_lowercase();
|
||||
if is_boolean_type(&data_type) && text.eq_ignore_ascii_case("true") {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ pub mod sqlserver;
|
|||
pub mod ssh_tunnel;
|
||||
pub mod transport_layer_tunnel;
|
||||
pub mod turso_driver;
|
||||
pub mod wkb;
|
||||
|
||||
use reqwest::ClientBuilder;
|
||||
use std::future::Future;
|
||||
|
|
|
|||
|
|
@ -284,7 +284,12 @@ fn mysql_value_to_json(row: &mysql_async::Row, idx: usize) -> serde_json::Value
|
|||
return row_get::<Vec<u8>, _>(row, idx)
|
||||
.map(|bytes| {
|
||||
if matches!(column.column_type(), ColumnType::MYSQL_TYPE_GEOMETRY) {
|
||||
mysql_blob_preview(&bytes, "GEOMETRY")
|
||||
// MySQL prefixes geometry WKB with a 4-byte SRID.
|
||||
// Strip it before passing to the WKB parser.
|
||||
let wkb = if bytes.len() >= 4 { &bytes[4..] } else { &bytes };
|
||||
super::wkb::wkb_to_wkt(wkb)
|
||||
.map(serde_json::Value::String)
|
||||
.unwrap_or_else(|| super::binary_value_to_json(&bytes))
|
||||
} else {
|
||||
mysql_bytes_to_json(bytes, column)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,309 +86,6 @@ impl<'a> FromSql<'a> for PgRawBytes {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
struct WkbDimensions {
|
||||
has_z: bool,
|
||||
has_m: bool,
|
||||
}
|
||||
|
||||
impl WkbDimensions {
|
||||
fn suffix(self) -> &'static str {
|
||||
match (self.has_z, self.has_m) {
|
||||
(false, false) => "",
|
||||
(true, false) => " Z",
|
||||
(false, true) => " M",
|
||||
(true, true) => " ZM",
|
||||
}
|
||||
}
|
||||
|
||||
fn coordinate_len(self) -> usize {
|
||||
2 + usize::from(self.has_z) + usize::from(self.has_m)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum WkbGeometry {
|
||||
Point { dims: WkbDimensions, coords: Option<Vec<f64>> },
|
||||
LineString { dims: WkbDimensions, points: Vec<Vec<f64>> },
|
||||
Polygon { dims: WkbDimensions, rings: Vec<Vec<Vec<f64>>> },
|
||||
MultiPoint { dims: WkbDimensions, points: Vec<Option<Vec<f64>>> },
|
||||
MultiLineString { dims: WkbDimensions, lines: Vec<Vec<Vec<f64>>> },
|
||||
MultiPolygon { dims: WkbDimensions, polygons: Vec<Vec<Vec<Vec<f64>>>> },
|
||||
GeometryCollection { dims: WkbDimensions, geometries: Vec<WkbGeometry> },
|
||||
}
|
||||
|
||||
impl WkbGeometry {
|
||||
fn to_wkt(&self) -> String {
|
||||
match self {
|
||||
Self::Point { dims, coords } => match coords {
|
||||
Some(coords) => format!("POINT{}({})", dims.suffix(), format_wkb_coordinate(coords)),
|
||||
None => format!("POINT{} EMPTY", dims.suffix()),
|
||||
},
|
||||
Self::LineString { dims, points } => {
|
||||
if points.is_empty() {
|
||||
format!("LINESTRING{} EMPTY", dims.suffix())
|
||||
} else {
|
||||
format!("LINESTRING{}({})", dims.suffix(), format_wkb_coordinate_sequence(points))
|
||||
}
|
||||
}
|
||||
Self::Polygon { dims, rings } => {
|
||||
if rings.is_empty() {
|
||||
format!("POLYGON{} EMPTY", dims.suffix())
|
||||
} else {
|
||||
format!(
|
||||
"POLYGON{}({})",
|
||||
dims.suffix(),
|
||||
rings
|
||||
.iter()
|
||||
.map(|ring| format!("({})", format_wkb_coordinate_sequence(ring)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
)
|
||||
}
|
||||
}
|
||||
Self::MultiPoint { dims, points } => {
|
||||
if points.is_empty() {
|
||||
format!("MULTIPOINT{} EMPTY", dims.suffix())
|
||||
} else {
|
||||
format!(
|
||||
"MULTIPOINT{}({})",
|
||||
dims.suffix(),
|
||||
points
|
||||
.iter()
|
||||
.map(|point| match point {
|
||||
Some(coords) => format!("({})", format_wkb_coordinate(coords)),
|
||||
None => "EMPTY".to_string(),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
)
|
||||
}
|
||||
}
|
||||
Self::MultiLineString { dims, lines } => {
|
||||
if lines.is_empty() {
|
||||
format!("MULTILINESTRING{} EMPTY", dims.suffix())
|
||||
} else {
|
||||
format!(
|
||||
"MULTILINESTRING{}({})",
|
||||
dims.suffix(),
|
||||
lines
|
||||
.iter()
|
||||
.map(|line| format!("({})", format_wkb_coordinate_sequence(line)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
)
|
||||
}
|
||||
}
|
||||
Self::MultiPolygon { dims, polygons } => {
|
||||
if polygons.is_empty() {
|
||||
format!("MULTIPOLYGON{} EMPTY", dims.suffix())
|
||||
} else {
|
||||
format!(
|
||||
"MULTIPOLYGON{}({})",
|
||||
dims.suffix(),
|
||||
polygons
|
||||
.iter()
|
||||
.map(|polygon| {
|
||||
format!(
|
||||
"({})",
|
||||
polygon
|
||||
.iter()
|
||||
.map(|ring| format!("({})", format_wkb_coordinate_sequence(ring)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
)
|
||||
}
|
||||
}
|
||||
Self::GeometryCollection { dims, geometries } => {
|
||||
if geometries.is_empty() {
|
||||
format!("GEOMETRYCOLLECTION{} EMPTY", dims.suffix())
|
||||
} else {
|
||||
format!(
|
||||
"GEOMETRYCOLLECTION{}({})",
|
||||
dims.suffix(),
|
||||
geometries.iter().map(WkbGeometry::to_wkt).collect::<Vec<_>>().join(",")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn format_wkb_coordinate(coords: &[f64]) -> String {
|
||||
coords.iter().map(|value| value.to_string()).collect::<Vec<_>>().join(" ")
|
||||
}
|
||||
|
||||
fn format_wkb_coordinate_sequence(points: &[Vec<f64>]) -> String {
|
||||
points.iter().map(|point| format_wkb_coordinate(point)).collect::<Vec<_>>().join(",")
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum WkbEndian {
|
||||
Big,
|
||||
Little,
|
||||
}
|
||||
|
||||
struct WkbReader<'a> {
|
||||
raw: &'a [u8],
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl<'a> WkbReader<'a> {
|
||||
fn new(raw: &'a [u8]) -> Self {
|
||||
Self { raw, pos: 0 }
|
||||
}
|
||||
|
||||
fn read_u8(&mut self) -> Option<u8> {
|
||||
let value = *self.raw.get(self.pos)?;
|
||||
self.pos += 1;
|
||||
Some(value)
|
||||
}
|
||||
|
||||
fn read_array<const N: usize>(&mut self) -> Option<[u8; N]> {
|
||||
let end = self.pos.checked_add(N)?;
|
||||
let bytes: [u8; N] = self.raw.get(self.pos..end)?.try_into().ok()?;
|
||||
self.pos = end;
|
||||
Some(bytes)
|
||||
}
|
||||
|
||||
fn read_u32(&mut self, endian: WkbEndian) -> Option<u32> {
|
||||
let bytes = self.read_array::<4>()?;
|
||||
Some(match endian {
|
||||
WkbEndian::Big => u32::from_be_bytes(bytes),
|
||||
WkbEndian::Little => u32::from_le_bytes(bytes),
|
||||
})
|
||||
}
|
||||
|
||||
fn read_f64(&mut self, endian: WkbEndian) -> Option<f64> {
|
||||
let bytes = self.read_array::<8>()?;
|
||||
Some(match endian {
|
||||
WkbEndian::Big => f64::from_be_bytes(bytes),
|
||||
WkbEndian::Little => f64::from_le_bytes(bytes),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_wkb_dimensions(type_word: u32) -> (u32, WkbDimensions, bool) {
|
||||
let mut base_type = type_word & 0x1FFF_FFFF;
|
||||
let mut dims = WkbDimensions { has_z: (type_word & 0x8000_0000) != 0, has_m: (type_word & 0x4000_0000) != 0 };
|
||||
let has_srid = (type_word & 0x2000_0000) != 0;
|
||||
|
||||
if base_type >= 3000 {
|
||||
dims.has_z = true;
|
||||
dims.has_m = true;
|
||||
base_type -= 3000;
|
||||
} else if base_type >= 2000 {
|
||||
dims.has_m = true;
|
||||
base_type -= 2000;
|
||||
} else if base_type >= 1000 {
|
||||
dims.has_z = true;
|
||||
base_type -= 1000;
|
||||
}
|
||||
|
||||
(base_type, dims, has_srid)
|
||||
}
|
||||
|
||||
fn read_wkb_point_coords(reader: &mut WkbReader<'_>, dims: WkbDimensions, allow_empty_point: bool) -> Option<Vec<f64>> {
|
||||
let endian = match reader.read_u8()? {
|
||||
0 => WkbEndian::Big,
|
||||
1 => WkbEndian::Little,
|
||||
_ => return None,
|
||||
};
|
||||
let type_word = reader.read_u32(endian)?;
|
||||
let (base_type, parsed_dims, has_srid) = parse_wkb_dimensions(type_word);
|
||||
if base_type != 1 || parsed_dims != dims {
|
||||
return None;
|
||||
}
|
||||
if has_srid {
|
||||
reader.read_u32(endian)?;
|
||||
}
|
||||
let coords = (0..parsed_dims.coordinate_len()).map(|_| reader.read_f64(endian)).collect::<Option<Vec<_>>>()?;
|
||||
if allow_empty_point && coords.iter().all(|value| value.is_nan()) {
|
||||
return None;
|
||||
}
|
||||
Some(coords)
|
||||
}
|
||||
|
||||
fn parse_wkb_points(reader: &mut WkbReader<'_>, endian: WkbEndian, dims: WkbDimensions) -> Option<Vec<Vec<f64>>> {
|
||||
let count = usize::try_from(reader.read_u32(endian)?).ok()?;
|
||||
(0..count)
|
||||
.map(|_| (0..dims.coordinate_len()).map(|_| reader.read_f64(endian)).collect::<Option<Vec<_>>>())
|
||||
.collect::<Option<Vec<_>>>()
|
||||
}
|
||||
|
||||
fn parse_wkb_geometry(reader: &mut WkbReader<'_>) -> Option<WkbGeometry> {
|
||||
let endian = match reader.read_u8()? {
|
||||
0 => WkbEndian::Big,
|
||||
1 => WkbEndian::Little,
|
||||
_ => return None,
|
||||
};
|
||||
let type_word = reader.read_u32(endian)?;
|
||||
let (base_type, dims, has_srid) = parse_wkb_dimensions(type_word);
|
||||
if has_srid {
|
||||
reader.read_u32(endian)?;
|
||||
}
|
||||
|
||||
match base_type {
|
||||
1 => {
|
||||
let coords = (0..dims.coordinate_len()).map(|_| reader.read_f64(endian)).collect::<Option<Vec<_>>>()?;
|
||||
let coords = if coords.iter().all(|value| value.is_nan()) { None } else { Some(coords) };
|
||||
Some(WkbGeometry::Point { dims, coords })
|
||||
}
|
||||
2 => Some(WkbGeometry::LineString { dims, points: parse_wkb_points(reader, endian, dims)? }),
|
||||
3 => {
|
||||
let ring_count = usize::try_from(reader.read_u32(endian)?).ok()?;
|
||||
let rings = (0..ring_count).map(|_| parse_wkb_points(reader, endian, dims)).collect::<Option<Vec<_>>>()?;
|
||||
Some(WkbGeometry::Polygon { dims, rings })
|
||||
}
|
||||
4 => {
|
||||
let count = usize::try_from(reader.read_u32(endian)?).ok()?;
|
||||
let points =
|
||||
(0..count).map(|_| Some(read_wkb_point_coords(reader, dims, true))).collect::<Option<Vec<_>>>()?;
|
||||
Some(WkbGeometry::MultiPoint { dims, points })
|
||||
}
|
||||
5 => {
|
||||
let count = usize::try_from(reader.read_u32(endian)?).ok()?;
|
||||
let lines = (0..count)
|
||||
.map(|_| match parse_wkb_geometry(reader)? {
|
||||
WkbGeometry::LineString { points, .. } => Some(points),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Option<Vec<_>>>()?;
|
||||
Some(WkbGeometry::MultiLineString { dims, lines })
|
||||
}
|
||||
6 => {
|
||||
let count = usize::try_from(reader.read_u32(endian)?).ok()?;
|
||||
let polygons = (0..count)
|
||||
.map(|_| match parse_wkb_geometry(reader)? {
|
||||
WkbGeometry::Polygon { rings, .. } => Some(rings),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Option<Vec<_>>>()?;
|
||||
Some(WkbGeometry::MultiPolygon { dims, polygons })
|
||||
}
|
||||
7 => {
|
||||
let count = usize::try_from(reader.read_u32(endian)?).ok()?;
|
||||
let geometries = (0..count).map(|_| parse_wkb_geometry(reader)).collect::<Option<Vec<_>>>()?;
|
||||
Some(WkbGeometry::GeometryCollection { dims, geometries })
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn ewkb_to_wkt(raw: &[u8]) -> Option<String> {
|
||||
let mut reader = WkbReader::new(raw);
|
||||
let geometry = parse_wkb_geometry(&mut reader)?;
|
||||
if reader.pos != raw.len() {
|
||||
return None;
|
||||
}
|
||||
Some(geometry.to_wkt())
|
||||
}
|
||||
|
||||
/// Decode pgvector binary format into a Vec<f32>.
|
||||
///
|
||||
/// pgvector binary layout (big-endian):
|
||||
|
|
@ -575,7 +272,7 @@ fn pg_value_to_json(row: &Row, idx: usize, type_name: &str) -> serde_json::Value
|
|||
|
||||
if upper == "GEOMETRY" || upper == "GEOGRAPHY" {
|
||||
if let Ok(PgRawBytes(raw)) = row.try_get::<_, PgRawBytes>(idx) {
|
||||
return ewkb_to_wkt(&raw)
|
||||
return super::wkb::wkb_to_wkt(&raw)
|
||||
.map(serde_json::Value::String)
|
||||
.unwrap_or_else(|| super::binary_value_to_json(&raw));
|
||||
}
|
||||
|
|
@ -2136,7 +1833,7 @@ mod tests {
|
|||
#[test]
|
||||
fn ewkb_point_with_srid_formats_as_wkt() {
|
||||
let raw = decode_hex("0101000020E6100000C520B07268195D404E62105839F44340");
|
||||
assert_eq!(ewkb_to_wkt(&raw), Some("POINT(116.397 39.908)".to_string()));
|
||||
assert_eq!(super::super::wkb::wkb_to_wkt(&raw), Some("POINT(116.397 39.908)".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -2145,7 +1842,7 @@ mod tests {
|
|||
"0106000020E610000002000000010300000001000000050000000000000000005D4000000000000044400000000000405D4000000000000044400000000000405D4000000000008044400000000000005D4000000000008044400000000000005D400000000000004440010300000001000000050000000000000000805D4000000000008043400000000000C05D4000000000008043400000000000C05D4000000000000044400000000000805D4000000000000044400000000000805D400000000000804340",
|
||||
);
|
||||
assert_eq!(
|
||||
ewkb_to_wkt(&raw),
|
||||
super::super::wkb::wkb_to_wkt(&raw),
|
||||
Some(
|
||||
"MULTIPOLYGON(((116 40,117 40,117 41,116 41,116 40)),((118 39,119 39,119 40,118 40,118 39)))"
|
||||
.to_string()
|
||||
|
|
@ -2158,7 +1855,10 @@ mod tests {
|
|||
let raw = decode_hex(
|
||||
"0107000020E61000000200000001010000000000000000005D4000000000000044400102000000020000000000000000405D4000000000008044400000000000805D400000000000004540",
|
||||
);
|
||||
assert_eq!(ewkb_to_wkt(&raw), Some("GEOMETRYCOLLECTION(POINT(116 40),LINESTRING(117 41,118 42))".to_string()));
|
||||
assert_eq!(
|
||||
super::super::wkb::wkb_to_wkt(&raw),
|
||||
Some("GEOMETRYCOLLECTION(POINT(116 40),LINESTRING(117 41,118 42))".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,312 @@
|
|||
/// Shared WKB (Well-Known Binary) geometry parsing shared by PostgreSQL (EWKB)
|
||||
/// and MySQL (standard WKB). Parses WKB bytes into WKT (Well-Known Text) strings.
|
||||
///
|
||||
/// Supports: POINT, LINESTRING, POLYGON, MULTIPOINT, MULTILINESTRING,
|
||||
/// MULTIPOLYGON, GEOMETRYCOLLECTION, and their Z/M/ZM variants.
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
struct WkbDimensions {
|
||||
has_z: bool,
|
||||
has_m: bool,
|
||||
}
|
||||
|
||||
impl WkbDimensions {
|
||||
fn suffix(self) -> &'static str {
|
||||
match (self.has_z, self.has_m) {
|
||||
(false, false) => "",
|
||||
(true, false) => " Z",
|
||||
(false, true) => " M",
|
||||
(true, true) => " ZM",
|
||||
}
|
||||
}
|
||||
|
||||
fn coordinate_len(self) -> usize {
|
||||
2 + usize::from(self.has_z) + usize::from(self.has_m)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum WkbGeometry {
|
||||
Point { dims: WkbDimensions, coords: Option<Vec<f64>> },
|
||||
LineString { dims: WkbDimensions, points: Vec<Vec<f64>> },
|
||||
Polygon { dims: WkbDimensions, rings: Vec<Vec<Vec<f64>>> },
|
||||
MultiPoint { dims: WkbDimensions, points: Vec<Option<Vec<f64>>> },
|
||||
MultiLineString { dims: WkbDimensions, lines: Vec<Vec<Vec<f64>>> },
|
||||
MultiPolygon { dims: WkbDimensions, polygons: Vec<Vec<Vec<Vec<f64>>>> },
|
||||
GeometryCollection { dims: WkbDimensions, geometries: Vec<WkbGeometry> },
|
||||
}
|
||||
|
||||
impl WkbGeometry {
|
||||
fn to_wkt(&self) -> String {
|
||||
match self {
|
||||
Self::Point { dims, coords } => match coords {
|
||||
Some(coords) => format!("POINT{}({})", dims.suffix(), format_wkb_coordinate(coords)),
|
||||
None => format!("POINT{} EMPTY", dims.suffix()),
|
||||
},
|
||||
Self::LineString { dims, points } => {
|
||||
if points.is_empty() {
|
||||
format!("LINESTRING{} EMPTY", dims.suffix())
|
||||
} else {
|
||||
format!("LINESTRING{}({})", dims.suffix(), format_wkb_coordinate_sequence(points))
|
||||
}
|
||||
}
|
||||
Self::Polygon { dims, rings } => {
|
||||
if rings.is_empty() {
|
||||
format!("POLYGON{} EMPTY", dims.suffix())
|
||||
} else {
|
||||
format!(
|
||||
"POLYGON{}({})",
|
||||
dims.suffix(),
|
||||
rings
|
||||
.iter()
|
||||
.map(|ring| format!("({})", format_wkb_coordinate_sequence(ring)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
)
|
||||
}
|
||||
}
|
||||
Self::MultiPoint { dims, points } => {
|
||||
if points.is_empty() {
|
||||
format!("MULTIPOINT{} EMPTY", dims.suffix())
|
||||
} else {
|
||||
format!(
|
||||
"MULTIPOINT{}({})",
|
||||
dims.suffix(),
|
||||
points
|
||||
.iter()
|
||||
.map(|point| match point {
|
||||
Some(coords) => format!("({})", format_wkb_coordinate(coords)),
|
||||
None => "EMPTY".to_string(),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
)
|
||||
}
|
||||
}
|
||||
Self::MultiLineString { dims, lines } => {
|
||||
if lines.is_empty() {
|
||||
format!("MULTILINESTRING{} EMPTY", dims.suffix())
|
||||
} else {
|
||||
format!(
|
||||
"MULTILINESTRING{}({})",
|
||||
dims.suffix(),
|
||||
lines
|
||||
.iter()
|
||||
.map(|line| format!("({})", format_wkb_coordinate_sequence(line)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
)
|
||||
}
|
||||
}
|
||||
Self::MultiPolygon { dims, polygons } => {
|
||||
if polygons.is_empty() {
|
||||
format!("MULTIPOLYGON{} EMPTY", dims.suffix())
|
||||
} else {
|
||||
format!(
|
||||
"MULTIPOLYGON{}({})",
|
||||
dims.suffix(),
|
||||
polygons
|
||||
.iter()
|
||||
.map(|polygon| {
|
||||
format!(
|
||||
"({})",
|
||||
polygon
|
||||
.iter()
|
||||
.map(|ring| format!("({})", format_wkb_coordinate_sequence(ring)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
)
|
||||
}
|
||||
}
|
||||
Self::GeometryCollection { dims, geometries } => {
|
||||
if geometries.is_empty() {
|
||||
format!("GEOMETRYCOLLECTION{} EMPTY", dims.suffix())
|
||||
} else {
|
||||
format!(
|
||||
"GEOMETRYCOLLECTION{}({})",
|
||||
dims.suffix(),
|
||||
geometries.iter().map(WkbGeometry::to_wkt).collect::<Vec<_>>().join(",")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn format_wkb_coordinate(coords: &[f64]) -> String {
|
||||
coords.iter().map(|value| value.to_string()).collect::<Vec<_>>().join(" ")
|
||||
}
|
||||
|
||||
fn format_wkb_coordinate_sequence(points: &[Vec<f64>]) -> String {
|
||||
points.iter().map(|point| format_wkb_coordinate(point)).collect::<Vec<_>>().join(",")
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum WkbEndian {
|
||||
Big,
|
||||
Little,
|
||||
}
|
||||
|
||||
struct WkbReader<'a> {
|
||||
raw: &'a [u8],
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl<'a> WkbReader<'a> {
|
||||
fn new(raw: &'a [u8]) -> Self {
|
||||
Self { raw, pos: 0 }
|
||||
}
|
||||
|
||||
fn read_u8(&mut self) -> Option<u8> {
|
||||
let value = *self.raw.get(self.pos)?;
|
||||
self.pos += 1;
|
||||
Some(value)
|
||||
}
|
||||
|
||||
fn read_array<const N: usize>(&mut self) -> Option<[u8; N]> {
|
||||
let end = self.pos.checked_add(N)?;
|
||||
let bytes: [u8; N] = self.raw.get(self.pos..end)?.try_into().ok()?;
|
||||
self.pos = end;
|
||||
Some(bytes)
|
||||
}
|
||||
|
||||
fn read_u32(&mut self, endian: WkbEndian) -> Option<u32> {
|
||||
let bytes = self.read_array::<4>()?;
|
||||
Some(match endian {
|
||||
WkbEndian::Big => u32::from_be_bytes(bytes),
|
||||
WkbEndian::Little => u32::from_le_bytes(bytes),
|
||||
})
|
||||
}
|
||||
|
||||
fn read_f64(&mut self, endian: WkbEndian) -> Option<f64> {
|
||||
let bytes = self.read_array::<8>()?;
|
||||
Some(match endian {
|
||||
WkbEndian::Big => f64::from_be_bytes(bytes),
|
||||
WkbEndian::Little => f64::from_le_bytes(bytes),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_wkb_dimensions(type_word: u32) -> (u32, WkbDimensions, bool) {
|
||||
let mut base_type = type_word & 0x1FFF_FFFF;
|
||||
let mut dims = WkbDimensions { has_z: (type_word & 0x8000_0000) != 0, has_m: (type_word & 0x4000_0000) != 0 };
|
||||
let has_srid = (type_word & 0x2000_0000) != 0;
|
||||
|
||||
if base_type >= 3000 {
|
||||
dims.has_z = true;
|
||||
dims.has_m = true;
|
||||
base_type -= 3000;
|
||||
} else if base_type >= 2000 {
|
||||
dims.has_m = true;
|
||||
base_type -= 2000;
|
||||
} else if base_type >= 1000 {
|
||||
dims.has_z = true;
|
||||
base_type -= 1000;
|
||||
}
|
||||
|
||||
(base_type, dims, has_srid)
|
||||
}
|
||||
|
||||
fn read_wkb_point_coords(reader: &mut WkbReader<'_>, dims: WkbDimensions, allow_empty_point: bool) -> Option<Vec<f64>> {
|
||||
let endian = match reader.read_u8()? {
|
||||
0 => WkbEndian::Big,
|
||||
1 => WkbEndian::Little,
|
||||
_ => return None,
|
||||
};
|
||||
let type_word = reader.read_u32(endian)?;
|
||||
let (base_type, parsed_dims, has_srid) = parse_wkb_dimensions(type_word);
|
||||
if base_type != 1 || parsed_dims != dims {
|
||||
return None;
|
||||
}
|
||||
if has_srid {
|
||||
reader.read_u32(endian)?;
|
||||
}
|
||||
let coords = (0..parsed_dims.coordinate_len()).map(|_| reader.read_f64(endian)).collect::<Option<Vec<_>>>()?;
|
||||
if allow_empty_point && coords.iter().all(|value| value.is_nan()) {
|
||||
return None;
|
||||
}
|
||||
Some(coords)
|
||||
}
|
||||
|
||||
fn parse_wkb_points(reader: &mut WkbReader<'_>, endian: WkbEndian, dims: WkbDimensions) -> Option<Vec<Vec<f64>>> {
|
||||
let count = usize::try_from(reader.read_u32(endian)?).ok()?;
|
||||
(0..count)
|
||||
.map(|_| (0..dims.coordinate_len()).map(|_| reader.read_f64(endian)).collect::<Option<Vec<_>>>())
|
||||
.collect::<Option<Vec<_>>>()
|
||||
}
|
||||
|
||||
fn parse_wkb_geometry(reader: &mut WkbReader<'_>) -> Option<WkbGeometry> {
|
||||
let endian = match reader.read_u8()? {
|
||||
0 => WkbEndian::Big,
|
||||
1 => WkbEndian::Little,
|
||||
_ => return None,
|
||||
};
|
||||
let type_word = reader.read_u32(endian)?;
|
||||
let (base_type, dims, has_srid) = parse_wkb_dimensions(type_word);
|
||||
if has_srid {
|
||||
reader.read_u32(endian)?;
|
||||
}
|
||||
|
||||
match base_type {
|
||||
1 => {
|
||||
let coords = (0..dims.coordinate_len()).map(|_| reader.read_f64(endian)).collect::<Option<Vec<_>>>()?;
|
||||
let coords = if coords.iter().all(|value| value.is_nan()) { None } else { Some(coords) };
|
||||
Some(WkbGeometry::Point { dims, coords })
|
||||
}
|
||||
2 => Some(WkbGeometry::LineString { dims, points: parse_wkb_points(reader, endian, dims)? }),
|
||||
3 => {
|
||||
let ring_count = usize::try_from(reader.read_u32(endian)?).ok()?;
|
||||
let rings = (0..ring_count).map(|_| parse_wkb_points(reader, endian, dims)).collect::<Option<Vec<_>>>()?;
|
||||
Some(WkbGeometry::Polygon { dims, rings })
|
||||
}
|
||||
4 => {
|
||||
let count = usize::try_from(reader.read_u32(endian)?).ok()?;
|
||||
let points =
|
||||
(0..count).map(|_| Some(read_wkb_point_coords(reader, dims, true))).collect::<Option<Vec<_>>>()?;
|
||||
Some(WkbGeometry::MultiPoint { dims, points })
|
||||
}
|
||||
5 => {
|
||||
let count = usize::try_from(reader.read_u32(endian)?).ok()?;
|
||||
let lines = (0..count)
|
||||
.map(|_| match parse_wkb_geometry(reader)? {
|
||||
WkbGeometry::LineString { points, .. } => Some(points),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Option<Vec<_>>>()?;
|
||||
Some(WkbGeometry::MultiLineString { dims, lines })
|
||||
}
|
||||
6 => {
|
||||
let count = usize::try_from(reader.read_u32(endian)?).ok()?;
|
||||
let polygons = (0..count)
|
||||
.map(|_| match parse_wkb_geometry(reader)? {
|
||||
WkbGeometry::Polygon { rings, .. } => Some(rings),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Option<Vec<_>>>()?;
|
||||
Some(WkbGeometry::MultiPolygon { dims, polygons })
|
||||
}
|
||||
7 => {
|
||||
let count = usize::try_from(reader.read_u32(endian)?).ok()?;
|
||||
let geometries = (0..count).map(|_| parse_wkb_geometry(reader)).collect::<Option<Vec<_>>>()?;
|
||||
Some(WkbGeometry::GeometryCollection { dims, geometries })
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse WKB/EWKB bytes into a WKT (Well-Known Text) string.
|
||||
///
|
||||
/// Supports both standard WKB (MySQL) and Extended WKB (PostgreSQL/PostGIS).
|
||||
/// Returns `None` if the bytes cannot be parsed as valid WKB.
|
||||
pub(crate) fn wkb_to_wkt(raw: &[u8]) -> Option<String> {
|
||||
let mut reader = WkbReader::new(raw);
|
||||
let geometry = parse_wkb_geometry(&mut reader)?;
|
||||
if reader.pos != raw.len() {
|
||||
return None;
|
||||
}
|
||||
Some(geometry.to_wkt())
|
||||
}
|
||||
Loading…
Reference in New Issue