Hello, everyone! I’ve recently started learning Rust, and this is my first post on this forum. I’d greatly appreciate any tips or suggestions you might have!
I was trying to learn something about bit manipulation and the core::memory
module in Rust. I started with a simple task of converting between the octet and decimal representations of IPv4 addresses. For instance, the IPv4 address 192.229.162.211
translates to 3236274899
in decimal. Similarly, 167824216
translates back to 10.0.203.88
. This was relatively easy.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct IPv4(u32);
impl IPv4 {
pub const fn from_octets(address: &[u8; 4]) -> Self {
Self(u32::from_be_bytes(*address))
}
pub const fn to_octets(self) -> [u8; 4] {
self.0.to_be_bytes()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ipv4_1a() {
let input = [192, 229, 162, 211];
let result = 3236274899;
assert_eq!(IPv4::from_octets(&input), IPv4(result));
}
#[test]
fn test_ipv4_1b() {
let input = 167824216;
let result = [10, 0, 203, 88];
assert_eq!(IPv4(input).to_octets(), result);
}
}
Then, I wanted to solve an analogous problem for IPv6. In this case, an address decomposes into segments rather than octets, so the previous approach doesn’t really work. In the end, I came up with the following code:
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct IPv6(u128);
impl IPv6 {
pub fn from_segments(address: &[u16; 8]) -> Self {
Self(if cfg!(target_endian = "big") {
unsafe { transmute::<[u16; 8], u128>(*address) }
} else {
unsafe { transmute::<[u16; 8], u128>(address.map(|x| x.reverse_bits())) }.reverse_bits()
})
}
pub fn to_segments(self) -> [u16; 8] {
if cfg!(target_endian = "big") {
unsafe { transmute::<u128, [u16; 8]>(self.0) }
} else {
unsafe { transmute::<u128, [u16; 8]>(self.0.reverse_bits()) }.map(|x| x.reverse_bits())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ipv6_1a() {
let input = [0x2607, 0xf8b0, 0x4005, 0x808, 0x0, 0x0, 0x0, 0x2004];
let result = 0x2607f8b0400508080000000000002004;
assert_eq!(IPv6::from_segments(&input), IPv6(result));
}
#[test]
fn test_ipv6_1b() {
let input = 0x20010db8000000000000ff0000428329;
let result = [0x2001, 0x0db8, 0x0, 0x0, 0x0, 0xff00, 0x0042, 0x8329];
assert_eq!(IPv6(input).to_segments(), result);
}
}
I am not really satisfied with my solution, though. Compared to the IPv4 implementation, it has several issues:
- It seems overengineered to me, given how simple the problem is (just bit manipulation, after all).
- I’m not sure if the code is portable: does it really work correctly on big-endian machines?
- I can no longer declare the functions as
const
, even though converting between representations is exactly something one would reasonably expect to be done at the compile time.
How could this code be improved? Many thanks!
edit: corrected typos.