fix: render binary values as hex (#465)

This commit is contained in:
vvrf authored and GitHub committed 2026-05-26 22:46:00 +08:00
1 parent 660ed3002f
commit 3a595c8efb
9 files changed
+128 -29

No files matched your search

+24
View File
@@ -45,6 +45,20 @@ pub fn safe_u64_to_json(v: u64) -> serde_json::Value {
}
}
pub(crate) fn hex_encode(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut out = String::with_capacity(bytes.len() * 2);
for &byte in bytes {
out.push(HEX[(byte >> 4) as usize] as char);
out.push(HEX[(byte & 0x0f) as usize] as char);
}
out
}
pub(crate) fn binary_value_to_json(bytes: &[u8]) -> serde_json::Value {
serde_json::Value::String(format!("0x{}", hex_encode(bytes)))
}
pub fn tcp_probe_timeout() -> Duration {
Duration::from_secs(TCP_PROBE_TIMEOUT_SECS)
}
@@ -89,3 +103,13 @@ pub async fn probe_tcp_endpoint(label: &str, host: &str, port: u16) -> Result<()
.map(|_| ())
.map_err(|e| format!("{label} TCP connection failed: {e}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn binary_values_are_displayed_as_prefixed_hex() {
assert_eq!(binary_value_to_json(&[0x00, 0x01, 0xab, 0xff]), serde_json::json!("0x0001abff"));
}
}
+42 -4
View File
@@ -1,6 +1,6 @@
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
use futures::StreamExt;
use mysql_async::consts::ColumnType;
use mysql_async::consts::{ColumnFlags, ColumnType};
use mysql_async::prelude::*;
use rust_decimal::Decimal;
use std::borrow::Cow;
@@ -84,6 +84,30 @@ fn is_lossless_integer_column(column: &mysql_async::Column) -> bool {
matches!(column.column_type(), ColumnType::MYSQL_TYPE_LONGLONG | ColumnType::MYSQL_TYPE_NEWDECIMAL)
}
fn is_binary_column(column: &mysql_async::Column) -> bool {
let binary_flag = column.flags().contains(ColumnFlags::BINARY_FLAG);
let binary_charset = column.character_set() == 63;
matches!(column.column_type(), ColumnType::MYSQL_TYPE_GEOMETRY)
|| ((binary_flag || binary_charset)
&& matches!(
column.column_type(),
ColumnType::MYSQL_TYPE_BLOB
| ColumnType::MYSQL_TYPE_LONG_BLOB
| ColumnType::MYSQL_TYPE_MEDIUM_BLOB
| ColumnType::MYSQL_TYPE_TINY_BLOB
| ColumnType::MYSQL_TYPE_STRING
| ColumnType::MYSQL_TYPE_VAR_STRING
| ColumnType::MYSQL_TYPE_VARCHAR
))
}
fn mysql_bytes_to_json(bytes: Vec<u8>, column: &mysql_async::Column) -> serde_json::Value {
if is_binary_column(column) {
return super::binary_value_to_json(&bytes);
}
serde_json::Value::String(String::from_utf8_lossy(&bytes).to_string())
}
fn mysql_value_to_json(row: &mysql_async::Row, idx: usize) -> serde_json::Value {
let Some(column) = row.columns_ref().get(idx) else {
return serde_json::Value::Null;
@@ -96,6 +120,12 @@ fn mysql_value_to_json(row: &mysql_async::Row, idx: usize) -> serde_json::Value
return serde_json::Value::Null;
}
if is_binary_column(column) {
return row_get::<Vec<u8>, _>(row, idx)
.map(|bytes| super::binary_value_to_json(&bytes))
.unwrap_or(serde_json::Value::Null);
}
match column.column_type() {
ColumnType::MYSQL_TYPE_JSON => {
if let Some(v) = row_get::<String, _>(row, idx) {
@@ -114,8 +144,7 @@ fn mysql_value_to_json(row: &mysql_async::Row, idx: usize) -> serde_json::Value
.or_else(|| row_get::<i64, _>(row, idx).map(|v| serde_json::Value::String(v.to_string())))
.or_else(|| row_get::<u64, _>(row, idx).map(|v| serde_json::Value::String(v.to_string())))
.or_else(|| {
row_get::<Vec<u8>, _>(row, idx)
.map(|b| serde_json::Value::String(String::from_utf8_lossy(&b).to_string()))
row_get::<Vec<u8>, _>(row, idx).map(|bytes| mysql_bytes_to_json(bytes, column))
})
.unwrap_or(serde_json::Value::Null);
}
@@ -133,6 +162,15 @@ fn mysql_value_to_json(row: &mysql_async::Row, idx: usize) -> serde_json::Value
})
.unwrap_or(serde_json::Value::Null);
}
ColumnType::MYSQL_TYPE_BLOB
| ColumnType::MYSQL_TYPE_LONG_BLOB
| ColumnType::MYSQL_TYPE_MEDIUM_BLOB
| ColumnType::MYSQL_TYPE_TINY_BLOB
| ColumnType::MYSQL_TYPE_GEOMETRY => {
return row_get::<Vec<u8>, _>(row, idx)
.map(|bytes| mysql_bytes_to_json(bytes, column))
.unwrap_or(serde_json::Value::Null);
}
ColumnType::MYSQL_TYPE_TIMESTAMP
| ColumnType::MYSQL_TYPE_TIMESTAMP2
| ColumnType::MYSQL_TYPE_DATETIME
@@ -167,7 +205,7 @@ fn mysql_value_to_json(row: &mysql_async::Row, idx: usize) -> serde_json::Value
})
.or_else(|| row_get::<bool, _>(row, idx).map(serde_json::Value::Bool))
.or_else(|| {
row_get::<Vec<u8>, _>(row, idx).map(|b| serde_json::Value::String(String::from_utf8_lossy(&b).to_string()))
row_get::<Vec<u8>, _>(row, idx).map(|bytes| mysql_bytes_to_json(bytes, column))
})
.unwrap_or(serde_json::Value::Null)
}
+8 -7
View File
@@ -124,6 +124,13 @@ fn pg_array_to_json_value(row: &Row, idx: usize) -> Option<serde_json::Value> {
fn pg_value_to_json(row: &Row, idx: usize, type_name: &str) -> serde_json::Value {
let upper = type_name.to_uppercase();
if upper == "BYTEA" {
return row
.try_get::<_, Vec<u8>>(idx)
.map(|bytes| super::binary_value_to_json(&bytes))
.unwrap_or(serde_json::Value::Null);
}
if upper == "JSON" || upper == "JSONB" {
if let Ok(v) = row.try_get::<_, serde_json::Value>(idx) {
return serde_json::Value::String(v.to_string());
@@ -195,13 +202,7 @@ fn pg_value_to_json(row: &Row, idx: usize, type_name: &str) -> serde_json::Value
.or_else(|_| row.try_get::<_, uuid::Uuid>(idx).map(|v| serde_json::Value::String(v.to_string())))
.or_else(|e| pg_temporal_to_json_value(row, idx).ok_or(e))
.or_else(|_| {
row.try_get::<_, Vec<u8>>(idx).map(|bytes| match std::str::from_utf8(&bytes) {
Ok(s) => serde_json::Value::String(s.to_string()),
Err(_) => {
let hex: String = bytes.iter().map(|b| format!("{:02x}", b)).collect();
serde_json::Value::String(hex)
}
})
row.try_get::<_, Vec<u8>>(idx).map(|bytes| super::binary_value_to_json(&bytes))
})
.unwrap_or(serde_json::Value::Null)
}
+1 -2
View File
@@ -1,4 +1,3 @@
use base64::prelude::{Engine as _, BASE64_STANDARD};
use rusqlite::types::ValueRef;
use rusqlite::{Connection, OpenFlags};
use std::path::Path;
@@ -542,6 +541,6 @@ fn value_ref_to_json(value: ValueRef<'_>) -> serde_json::Value {
serde_json::Number::from_f64(v).map(serde_json::Value::Number).unwrap_or(serde_json::Value::Null)
}
ValueRef::Text(v) => serde_json::Value::String(String::from_utf8_lossy(v).to_string()),
ValueRef::Blob(v) => serde_json::Value::String(BASE64_STANDARD.encode(v)),
ValueRef::Blob(v) => super::binary_value_to_json(v),
}
}
+1 -11
View File
@@ -241,21 +241,11 @@ fn sqlserver_cell_to_json(cell: &ColumnData<'static>) -> serde_json::Value {
return serde_json::Value::String(v.to_string());
}
if let Ok(Some(v)) = <Vec<u8> as tiberius::FromSqlOwned>::from_sql_owned(cell.clone()) {
return serde_json::Value::String(format!("0x{}", hex_encode(&v)));
return super::binary_value_to_json(&v);
}
serde_json::Value::Null
}
fn hex_encode(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut out = String::with_capacity(bytes.len() * 2);
for byte in bytes {
out.push(HEX[(byte >> 4) as usize] as char);
out.push(HEX[(byte & 0x0f) as usize] as char);
}
out
}
pub async fn list_databases(client: &mut SqlServerClient) -> Result<Vec<DatabaseInfo>, String> {
let stream = client
.query(