Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
parse and format ipv6 addresses in square brackets
Merged Defelo opened 2 months ago

Resolves ca00dda607acdb9b5e909a86b2e8f0d817f9718c

5 files changed +83 -30 7c923608 9ff67562
modified CHANGELOG.md
@@ -36,6 +36,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
  This would result in timeouts when commands are run from the `rad` CLI.
  The `Service` has now learned to return results when an error occurs which
  will be reported back to the user.
+
- The parsing of IPv6 addresses failed if they were enclosed in square brackets
+
  (e.g. in `rad node connect z6Mk...@[::1]:8776`). This has been fixed so that
+
  IPv6 addresses are parsed correctly both with and without square brackets.

## Deprecations

modified crates/radicle-cli/src/commands/node/control.rs
@@ -384,17 +384,17 @@ pub fn sessions(node: &Node) -> Result<Option<term::Table<5, term::Label>>, node
                term::Label::blank(),
            ),
            node::State::Attempted => (
-
                term::format::addr_compact(&sess.addr).into(),
+
                sess.addr.display_compact().to_string().into(),
                term::Label::from(state_attempted()),
                term::Label::blank(),
            ),
            node::State::Connected { since, .. } => (
-
                term::format::addr_compact(&sess.addr).into(),
+
                sess.addr.display_compact().to_string().into(),
                term::Label::from(state_connected()),
                term::format::dim(now - since).into(),
            ),
            node::State::Disconnected { since, .. } => (
-
                term::format::addr_compact(&sess.addr).into(),
+
                sess.addr.display_compact().to_string().into(),
                term::Label::from(state_disconnected()),
                term::format::dim(now - since).into(),
            ),
modified crates/radicle-cli/src/commands/sync.rs
@@ -460,7 +460,7 @@ impl FetcherSpinner {
            term::format::secondary(progress.succeeded()),
            term::format::secondary(self.replicas.lower_bound()),
            term::format::tertiary(term::format::node_id_human_compact(node)),
-
            term::format::tertiary(term::format::addr_compact(addr)),
+
            term::format::tertiary(addr.display_compact()),
        ))
    }

@@ -477,7 +477,7 @@ impl FetcherSpinner {
            term::format::secondary(progress.succeeded()),
            term::format::secondary(self.replicas.lower_bound()),
            term::format::tertiary(term::format::node_id_human_compact(node)),
-
            term::format::tertiary(term::format::addr_compact(addr)),
+
            term::format::tertiary(addr.display_compact()),
        ))
    }

modified crates/radicle-cli/src/terminal/format.rs
@@ -8,7 +8,7 @@ pub use radicle_term::{style, Paint};
use radicle::cob::ObjectId;
use radicle::identity::Visibility;
use radicle::node::policy::Policy;
-
use radicle::node::{Address, Alias, AliasStore, HostName, NodeId};
+
use radicle::node::{Alias, AliasStore, NodeId};
use radicle::prelude::Did;
use radicle::profile::{env, Profile};
use radicle::storage::RefUpdate;
@@ -32,28 +32,6 @@ pub fn node_id_human(node: &NodeId) -> Paint<String> {
    Paint::new(node.to_human())
}

-
#[must_use]
-
pub fn addr_compact(address: &Address) -> Paint<String> {
-
    let host = match address.host() {
-
        HostName::Ip(ip) => ip.to_string(),
-
        HostName::Dns(dns) => dns.clone(),
-
        HostName::Tor(onion) => {
-
            let onion = onion.to_string();
-
            let start = onion.chars().take(8).collect::<String>();
-
            let end = onion
-
                .chars()
-
                .skip(onion.len() - 8 - ".onion".len())
-
                .collect::<String>();
-
            format!("{start}…{end}")
-
        }
-
        _ => unreachable!(),
-
    };
-

-
    let port = address.port().to_string();
-

-
    Paint::new(format!("{host}:{port}"))
-
}
-

/// Format a git Oid.
pub fn oid(oid: impl Into<radicle::git::Oid>) -> Paint<String> {
    Paint::new(format!("{:.7}", oid.into()))
modified crates/radicle/src/node.rs
@@ -17,8 +17,10 @@ pub mod sync;
pub mod timestamp;

use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
+
use std::fmt::Display;
use std::io::{BufRead, BufReader};
use std::marker::PhantomData;
+
use std::net::Ipv6Addr;
use std::ops::{ControlFlow, Deref};
use std::path::{Path, PathBuf};
use std::str::FromStr;
@@ -30,7 +32,7 @@ use std::os::unix::net::UnixStream;
use uds_windows::UnixStream;

use amplify::WrapperMut;
-
use cyphernet::addr::NetAddr;
+
use cyphernet::addr::{AddrParseError, NetAddr};
use localtime::{LocalDuration, LocalTime};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
@@ -422,7 +424,7 @@ impl TryFrom<&sqlite::Value> for Alias {

/// Peer public protocol address.
#[derive(Clone, Eq, PartialEq, Debug, Hash, From, Wrapper, WrapperMut, Serialize, Deserialize)]
-
#[wrapper(Deref, Display, FromStr)]
+
#[wrapper(Deref)]
#[wrapper_mut(DerefMut)]
#[cfg_attr(
    feature = "schemars",
@@ -488,6 +490,60 @@ impl Address {
    pub fn port(&self) -> u16 {
        self.0.port
    }
+

+
    pub fn display_compact(&self) -> impl Display {
+
        let host = match self.host() {
+
            HostName::Ip(net::IpAddr::V4(ip)) => ip.to_string(),
+
            HostName::Ip(net::IpAddr::V6(ip)) => format!("[{ip}]"),
+
            HostName::Dns(dns) => dns.clone(),
+
            HostName::Tor(onion) => {
+
                let onion = onion.to_string();
+
                let start = onion.chars().take(8).collect::<String>();
+
                let end = onion
+
                    .chars()
+
                    .skip(onion.len() - 8 - ".onion".len())
+
                    .collect::<String>();
+
                format!("{start}…{end}")
+
            }
+
            _ => unreachable!(),
+
        };
+

+
        let port = self.port().to_string();
+

+
        format!("{host}:{port}")
+
    }
+
}
+

+
impl Display for Address {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        match self.host() {
+
            HostName::Ip(net::IpAddr::V6(ip)) => {
+
                write!(f, "[{ip}]:{}", self.port())
+
            }
+
            _ => self.0.fmt(f),
+
        }
+
    }
+
}
+

+
impl FromStr for Address {
+
    type Err = AddrParseError;
+

+
    fn from_str(s: &str) -> Result<Self, Self::Err> {
+
        let (host, port) = s.rsplit_once(':').ok_or(AddrParseError::PortAbsent)?;
+

+
        let host = if let Some(host) = host
+
            .strip_prefix('[')
+
            .and_then(|host| host.strip_suffix(']'))
+
        {
+
            HostName::Ip(host.parse::<Ipv6Addr>()?.into())
+
        } else {
+
            host.parse()?
+
        };
+

+
        let port = port.parse().map_err(|_| AddrParseError::InvalidPort)?;
+

+
        Ok(Self(NetAddr::new(host, port)))
+
    }
}

impl cyphernet::addr::Host for Address {
@@ -1449,6 +1505,22 @@ mod test {
    }

    #[test]
+
    fn test_address() {
+
        assert!(Address::from_str("127.0.0.1:8776").is_ok());
+
        assert!(Address::from_str("::1:8776").is_ok());
+
        assert!(Address::from_str("[::1]:8776").is_ok());
+
        assert!(Address::from_str("[::ffff:127.0.0.1]:8776").is_ok());
+
        assert!(Address::from_str("localhost:8776").is_ok());
+

+
        assert!(Address::from_str("").is_err());
+
        assert!(Address::from_str(":").is_err());
+
        assert!(Address::from_str("127.0.0.1").is_err());
+
        assert!(Address::from_str("127.0.0.1:xyz").is_err());
+
        assert!(Address::from_str("[invalid]:8776").is_err());
+
        assert!(Address::from_str("[127.0.0.1]:8776").is_err());
+
    }
+

+
    #[test]
    fn test_command_result() {
        #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
        struct Test {