feat: add ipnet support (#3710)

* feat: add ipnet support

* fix: ipnet not decoding IP address strings

* fix: prefer ipnetwork to ipnet for compatibility

* fix: unnecessary cfg
This commit is contained in:
Beau Gieskens
2025-03-24 10:19:05 +10:00
committed by GitHub
parent e474be6d4b
commit 1c9cbe939a
18 changed files with 293 additions and 6 deletions

View File

@@ -19,6 +19,7 @@ offline = ["sqlx-core/offline"]
bigdecimal = ["dep:bigdecimal", "dep:num-bigint", "sqlx-core/bigdecimal"]
bit-vec = ["dep:bit-vec", "sqlx-core/bit-vec"]
chrono = ["dep:chrono", "sqlx-core/chrono"]
ipnet = ["dep:ipnet", "sqlx-core/ipnet"]
ipnetwork = ["dep:ipnetwork", "sqlx-core/ipnetwork"]
mac_address = ["dep:mac_address", "sqlx-core/mac_address"]
rust_decimal = ["dep:rust_decimal", "rust_decimal/maths", "sqlx-core/rust_decimal"]
@@ -43,6 +44,7 @@ sha2 = { version = "0.10.0", default-features = false }
bigdecimal = { workspace = true, optional = true }
bit-vec = { workspace = true, optional = true }
chrono = { workspace = true, optional = true }
ipnet = { workspace = true, optional = true }
ipnetwork = { workspace = true, optional = true }
mac_address = { workspace = true, optional = true }
rust_decimal = { workspace = true, optional = true }

View File

@@ -88,6 +88,9 @@ impl_type_checking!(
#[cfg(feature = "ipnetwork")]
sqlx::types::ipnetwork::IpNetwork,
#[cfg(feature = "ipnet")]
sqlx::types::ipnet::IpNet,
#[cfg(feature = "mac_address")]
sqlx::types::mac_address::MacAddress,
@@ -149,6 +152,9 @@ impl_type_checking!(
#[cfg(feature = "ipnetwork")]
Vec<sqlx::types::ipnetwork::IpNetwork> | &[sqlx::types::ipnetwork::IpNetwork],
#[cfg(feature = "ipnet")]
Vec<sqlx::types::ipnet::IpNet> | &[sqlx::types::ipnet::IpNet],
#[cfg(feature = "mac_address")]
Vec<sqlx::types::mac_address::MacAddress> | &[sqlx::types::mac_address::MacAddress],

View File

@@ -0,0 +1,62 @@
use std::net::IpAddr;
use ipnet::IpNet;
use crate::decode::Decode;
use crate::encode::{Encode, IsNull};
use crate::error::BoxDynError;
use crate::types::Type;
use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueRef, Postgres};
impl Type<Postgres> for IpAddr
where
IpNet: Type<Postgres>,
{
fn type_info() -> PgTypeInfo {
IpNet::type_info()
}
fn compatible(ty: &PgTypeInfo) -> bool {
IpNet::compatible(ty)
}
}
impl PgHasArrayType for IpAddr {
fn array_type_info() -> PgTypeInfo {
<IpNet as PgHasArrayType>::array_type_info()
}
fn array_compatible(ty: &PgTypeInfo) -> bool {
<IpNet as PgHasArrayType>::array_compatible(ty)
}
}
impl<'db> Encode<'db, Postgres> for IpAddr
where
IpNet: Encode<'db, Postgres>,
{
fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
IpNet::from(*self).encode_by_ref(buf)
}
fn size_hint(&self) -> usize {
IpNet::from(*self).size_hint()
}
}
impl<'db> Decode<'db, Postgres> for IpAddr
where
IpNet: Decode<'db, Postgres>,
{
fn decode(value: PgValueRef<'db>) -> Result<Self, BoxDynError> {
let ipnet = IpNet::decode(value)?;
if matches!(ipnet, IpNet::V4(net) if net.prefix_len() != 32)
|| matches!(ipnet, IpNet::V6(net) if net.prefix_len() != 128)
{
Err("lossy decode from inet/cidr")?
}
Ok(ipnet.addr())
}
}

View File

@@ -0,0 +1,130 @@
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
#[cfg(feature = "ipnet")]
use ipnet::{IpNet, Ipv4Net, Ipv6Net};
use crate::decode::Decode;
use crate::encode::{Encode, IsNull};
use crate::error::BoxDynError;
use crate::types::Type;
use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres};
// https://github.com/postgres/postgres/blob/574925bfd0a8175f6e161936ea11d9695677ba09/src/include/utils/inet.h#L39
// Technically this is a magic number here but it doesn't make sense to drag in the whole of `libc`
// just for one constant.
const PGSQL_AF_INET: u8 = 2; // AF_INET
const PGSQL_AF_INET6: u8 = PGSQL_AF_INET + 1;
impl Type<Postgres> for IpNet {
fn type_info() -> PgTypeInfo {
PgTypeInfo::INET
}
fn compatible(ty: &PgTypeInfo) -> bool {
*ty == PgTypeInfo::CIDR || *ty == PgTypeInfo::INET
}
}
impl PgHasArrayType for IpNet {
fn array_type_info() -> PgTypeInfo {
PgTypeInfo::INET_ARRAY
}
fn array_compatible(ty: &PgTypeInfo) -> bool {
*ty == PgTypeInfo::CIDR_ARRAY || *ty == PgTypeInfo::INET_ARRAY
}
}
impl Encode<'_, Postgres> for IpNet {
fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
// https://github.com/postgres/postgres/blob/574925bfd0a8175f6e161936ea11d9695677ba09/src/backend/utils/adt/network.c#L293
// https://github.com/postgres/postgres/blob/574925bfd0a8175f6e161936ea11d9695677ba09/src/backend/utils/adt/network.c#L271
match self {
IpNet::V4(net) => {
buf.push(PGSQL_AF_INET); // ip_family
buf.push(net.prefix_len()); // ip_bits
buf.push(0); // is_cidr
buf.push(4); // nb (number of bytes)
buf.extend_from_slice(&net.addr().octets()) // address
}
IpNet::V6(net) => {
buf.push(PGSQL_AF_INET6); // ip_family
buf.push(net.prefix_len()); // ip_bits
buf.push(0); // is_cidr
buf.push(16); // nb (number of bytes)
buf.extend_from_slice(&net.addr().octets()); // address
}
}
Ok(IsNull::No)
}
fn size_hint(&self) -> usize {
match self {
IpNet::V4(_) => 8,
IpNet::V6(_) => 20,
}
}
}
impl Decode<'_, Postgres> for IpNet {
fn decode(value: PgValueRef<'_>) -> Result<Self, BoxDynError> {
let bytes = match value.format() {
PgValueFormat::Binary => value.as_bytes()?,
PgValueFormat::Text => {
let s = value.as_str()?;
println!("{s}");
if s.contains('/') {
return Ok(s.parse()?);
}
// IpNet::from_str doesn't handle conversion from IpAddr to IpNet
let addr: IpAddr = s.parse()?;
return Ok(addr.into());
}
};
if bytes.len() >= 8 {
let family = bytes[0];
let prefix = bytes[1];
let _is_cidr = bytes[2] != 0;
let len = bytes[3];
match family {
PGSQL_AF_INET => {
if bytes.len() == 8 && len == 4 {
let inet = Ipv4Net::new(
Ipv4Addr::new(bytes[4], bytes[5], bytes[6], bytes[7]),
prefix,
)?;
return Ok(IpNet::V4(inet));
}
}
PGSQL_AF_INET6 => {
if bytes.len() == 20 && len == 16 {
let inet = Ipv6Net::new(
Ipv6Addr::from([
bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], bytes[9],
bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
bytes[16], bytes[17], bytes[18], bytes[19],
]),
prefix,
)?;
return Ok(IpNet::V6(inet));
}
}
_ => {
return Err(format!("unknown ip family {family}").into());
}
}
}
Err("invalid data received when expecting an INET".into())
}
}

View File

@@ -0,0 +1,7 @@
// Prefer `ipnetwork` over `ipnet` because it was implemented first (want to avoid breaking change).
#[cfg(not(feature = "ipnetwork"))]
mod ipaddr;
// Parent module is named after the `ipnet` crate, this is named after the `IpNet` type.
#[allow(clippy::module_inception)]
mod ipnet;

View File

@@ -0,0 +1,5 @@
mod ipaddr;
// Parent module is named after the `ipnetwork` crate, this is named after the `IpNetwork` type.
#[allow(clippy::module_inception)]
mod ipnetwork;

View File

@@ -87,7 +87,7 @@
//!
//! ### [`ipnetwork`](https://crates.io/crates/ipnetwork)
//!
//! Requires the `ipnetwork` Cargo feature flag.
//! Requires the `ipnetwork` Cargo feature flag (takes precedence over `ipnet` if both are used).
//!
//! | Rust type | Postgres type(s) |
//! |---------------------------------------|------------------------------------------------------|
@@ -100,6 +100,17 @@
//!
//! `IpNetwork` does not have this limitation.
//!
//! ### [`ipnet`](https://crates.io/crates/ipnet)
//!
//! Requires the `ipnet` Cargo feature flag.
//!
//! | Rust type | Postgres type(s) |
//! |---------------------------------------|------------------------------------------------------|
//! | `ipnet::IpNet` | INET, CIDR |
//! | `std::net::IpAddr` | INET, CIDR |
//!
//! The same `IpAddr` limitation for smaller network prefixes applies as with `ipnet`.
//!
//! ### [`mac_address`](https://crates.io/crates/mac_address)
//!
//! Requires the `mac_address` Cargo feature flag.
@@ -248,11 +259,11 @@ mod time;
#[cfg(feature = "uuid")]
mod uuid;
#[cfg(feature = "ipnetwork")]
mod ipnetwork;
#[cfg(feature = "ipnet")]
mod ipnet;
#[cfg(feature = "ipnetwork")]
mod ipaddr;
mod ipnetwork;
#[cfg(feature = "mac_address")]
mod mac_address;