From 0b690800157587c88d037fa2aae9b48d5674a672 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Sat, 15 Oct 2022 23:41:59 +0200 Subject: [PATCH 1/2] Implement no-alloc formatting of IPs This allows removing the dependency on a global allocator, which is important for many embedded projects that want to avoid dynamic allocation. --- Cargo.toml | 4 +- src/commands.rs | 96 +++++++++++++++++++++++++++++++++++++++++++--- src/lib.rs | 1 + src/tests/stack.rs | 2 +- 4 files changed, 96 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index aa5139e..a93f7b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,8 @@ fugit = "0.3.6" fugit-timer = "0.1.3" heapless = "0.7.16" bbqueue = { version = "0.5.0", optional=true } +numtoa = "0.2" +base16 = { version = "0.2", default-features = false } [dev-dependencies] mockall = "0.11.2" @@ -36,4 +38,4 @@ log = ['atat/log'] thumbv6 = ['bbqueue/thumbv6'] # Contains mocks for doc examples and may be disabled for production. -examples = [] \ No newline at end of file +examples = [] diff --git a/src/commands.rs b/src/commands.rs index b9e536d..7a33132 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,12 +1,16 @@ +use core::fmt::Write; + use crate::responses::LocalAddressResponse; use crate::responses::NoResponse; use crate::stack::Error as StackError; use crate::wifi::{AddressErrors, JoinError}; -use alloc::string::ToString; use atat::atat_derive::AtatCmd; use atat::heapless::{String, Vec}; use atat::{AtatCmd, Error as AtError, InternalError}; -use embedded_nal::{SocketAddrV4, SocketAddrV6}; +use embedded_nal::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6}; +use numtoa::NumToA; + +const MAX_IP_LENGTH: usize = 39; // IPv4: 15, IPv6: 39 /// Trait for mapping command errors pub trait CommandErrorHandler { @@ -156,19 +160,50 @@ pub struct ConnectCommand { connection_type: String<5>, /// Remote IPv4 or IPV6 address - remote_host: String<39>, + remote_host: String, /// Remote port port: u16, } +/// Convert a `IPv4Addr` to a heapless `String` +fn ipv4_to_string(ip: &Ipv4Addr) -> String { + let mut ip_string = String::new(); + let mut num_buf = [0u8; 3]; + for (i, octet) in ip.octets().iter().enumerate() { + ip_string.write_str(octet.numtoa_str(10, &mut num_buf)).unwrap(); + if i != 3 { + ip_string.write_char('.').unwrap(); + } + } + ip_string +} + +/// Convert a `SocketAddrV6` IP to a heapless `String` +fn ipv6_to_string(ip: &Ipv6Addr) -> String { + let mut ip_string = String::new(); + let mut hex_buf = [0u8; 4]; + for (i, segment) in ip.segments().iter().enumerate() { + // Hex-encode IPv6 segment + base16::encode_config_slice(&segment.to_be_bytes(), base16::EncodeLower, &mut hex_buf); + ip_string + // Safety: The result from hex-encoding will always be valid UTF-8 + .write_str(unsafe { core::str::from_utf8_unchecked(&hex_buf) }) + .unwrap(); + if i != 7 { + ip_string.write_char(':').unwrap(); + } + } + ip_string +} + impl ConnectCommand { /// Establishes a IPv4 TCP connection pub fn tcp_v4(link_id: usize, remote: SocketAddrV4) -> Self { Self { link_id, connection_type: String::from("TCP"), - remote_host: String::from(remote.ip().to_string().as_str()), + remote_host: ipv4_to_string(remote.ip()), port: remote.port(), } } @@ -178,7 +213,7 @@ impl ConnectCommand { Self { link_id, connection_type: String::from("TCPv6"), - remote_host: String::from(remote.ip().to_string().as_str()), + remote_host: ipv6_to_string(remote.ip()), port: remote.port(), } } @@ -302,3 +337,54 @@ impl CommandErrorHandler for CloseSocketCommand { StackError::CloseError(error) } } + +#[cfg(test)] +mod tests { + use embedded_nal::{Ipv4Addr, Ipv6Addr}; + use heapless::String; + + use super::MAX_IP_LENGTH; + + macro_rules! test_v4 { + ($a:expr, $b:expr, $c:expr, $d:expr, $string:literal) => {{ + assert_eq!( + super::ipv4_to_string(&Ipv4Addr::new($a, $b, $c, $d)), + String::::from($string) + ); + }}; + } + + macro_rules! test_v6 { + ($a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr, $h:expr, $string:literal) => {{ + assert_eq!( + super::ipv6_to_string(&Ipv6Addr::new($a, $b, $c, $d, $e, $f, $g, $h)), + String::::from($string) + ); + }}; + } + + #[test] + fn test_ipv4_to_string() { + test_v4!(127, 0, 0, 1, "127.0.0.1"); + test_v4!(0, 0, 0, 0, "0.0.0.0"); + test_v4!(255, 255, 255, 0, "255.255.255.0"); + test_v4!(255, 255, 255, 255, "255.255.255.255"); + test_v4!(1, 2, 3, 4, "1.2.3.4"); + } + + #[test] + fn test_ipv6_to_string() { + test_v6!(0, 0, 0, 0, 0, 0, 0, 1, "0000:0000:0000:0000:0000:0000:0000:0001"); // ::1 + test_v6!( + 0x0102, + 0xaabb, + 0xffff, + 0x4242, + 0x0000, + 0x1111, + 0x2222, + 0x3333, + "0102:aabb:ffff:4242:0000:1111:2222:3333" + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index 2373bd1..fa4b601 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,6 +38,7 @@ #![cfg_attr(not(test), no_std)] #![cfg_attr(feature = "strict", deny(warnings))] +#[cfg(test)] extern crate alloc; pub(crate) mod commands; diff --git a/src/tests/stack.rs b/src/tests/stack.rs index 40dc598..30e6167 100644 --- a/src/tests/stack.rs +++ b/src/tests/stack.rs @@ -178,7 +178,7 @@ fn test_connect_correct_commands_ipv6() { assert_eq!(3, commands.len()); assert_eq!("AT+CIPRECVMODE=1\r\n".to_string(), commands[1]); assert_eq!( - "AT+CIPSTART=0,\"TCPv6\",\"2001:db8::1\",8080\r\n".to_string(), + "AT+CIPSTART=0,\"TCPv6\",\"2001:0db8:0000:0000:0000:0000:0000:0001\",8080\r\n".to_string(), commands[2] ); } From 5d04af1713c63b3411b9e61d69970f2abf1edbe1 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Sat, 15 Oct 2022 23:50:37 +0200 Subject: [PATCH 2/2] IPv6: Implement shortening of all-zero segments --- src/commands.rs | 24 ++++++++++++++++-------- src/tests/stack.rs | 2 +- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 7a33132..5754a84 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -184,12 +184,20 @@ fn ipv6_to_string(ip: &Ipv6Addr) -> String { let mut ip_string = String::new(); let mut hex_buf = [0u8; 4]; for (i, segment) in ip.segments().iter().enumerate() { - // Hex-encode IPv6 segment - base16::encode_config_slice(&segment.to_be_bytes(), base16::EncodeLower, &mut hex_buf); - ip_string - // Safety: The result from hex-encoding will always be valid UTF-8 - .write_str(unsafe { core::str::from_utf8_unchecked(&hex_buf) }) - .unwrap(); + // Write segment (hexadectet) + if segment == &0 { + // All-zero-segments can be shortened + ip_string.write_str("0").unwrap() + } else { + // Hex-encode IPv6 segment + base16::encode_config_slice(&segment.to_be_bytes(), base16::EncodeLower, &mut hex_buf); + ip_string + // Safety: The result from hex-encoding will always be valid UTF-8 + .write_str(unsafe { core::str::from_utf8_unchecked(&hex_buf) }) + .unwrap(); + } + + // Write separator if i != 7 { ip_string.write_char(':').unwrap(); } @@ -374,7 +382,7 @@ mod tests { #[test] fn test_ipv6_to_string() { - test_v6!(0, 0, 0, 0, 0, 0, 0, 1, "0000:0000:0000:0000:0000:0000:0000:0001"); // ::1 + test_v6!(0, 0, 0, 0, 0, 0, 0, 1, "0:0:0:0:0:0:0:0001"); // ::1 test_v6!( 0x0102, 0xaabb, @@ -384,7 +392,7 @@ mod tests { 0x1111, 0x2222, 0x3333, - "0102:aabb:ffff:4242:0000:1111:2222:3333" + "0102:aabb:ffff:4242:0:1111:2222:3333" ); } } diff --git a/src/tests/stack.rs b/src/tests/stack.rs index 30e6167..b6ddab5 100644 --- a/src/tests/stack.rs +++ b/src/tests/stack.rs @@ -178,7 +178,7 @@ fn test_connect_correct_commands_ipv6() { assert_eq!(3, commands.len()); assert_eq!("AT+CIPRECVMODE=1\r\n".to_string(), commands[1]); assert_eq!( - "AT+CIPSTART=0,\"TCPv6\",\"2001:0db8:0000:0000:0000:0000:0000:0001\",8080\r\n".to_string(), + "AT+CIPSTART=0,\"TCPv6\",\"2001:0db8:0:0:0:0:0:0001\",8080\r\n".to_string(), commands[2] ); }