diff --git a/.gitignore b/.gitignore index 5071d14..5057d9e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ target Cargo.lock -*.rs.bk \ No newline at end of file +*.rs.bk diff --git a/CHANGELOG.md b/CHANGELOG.md index 226f1de..cf15eca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.16.0 [unreleased] + +- Create `protocol_stack` for Multiaddr. See [PR 60] + # 0.15.0 [2022-10-20] - Add `WebRTC` instance for `Multiaddr`. See [PR 59]. diff --git a/Cargo.toml b/Cargo.toml index 4b493f9..7d5ef9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ keywords = ["multiaddr", "ipfs"] license = "MIT" name = "multiaddr" readme = "README.md" -version = "0.15.0" +version = "0.16.0" [features] default = ["url"] diff --git a/src/lib.rs b/src/lib.rs index f0e2196..e79e385 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -191,6 +191,13 @@ impl Multiaddr { } self.bytes[(n - m)..] == other.bytes[..] } + + /// Returns &str identifiers for the protocol names themselves. + /// This omits specific info like addresses, ports, peer IDs, and the like. + /// Example: `"/ip4/127.0.0.1/tcp/5001"` would return `["ip4", "tcp"]` + pub fn protocol_stack(&self) -> ProtoStackIter { + ProtoStackIter { parts: self.iter() } + } } impl fmt::Debug for Multiaddr { @@ -293,6 +300,18 @@ impl<'a> Iterator for Iter<'a> { } } +/// Iterator over the string idtenfiers of the protocols (not addrs) in a multiaddr +pub struct ProtoStackIter<'a> { + parts: Iter<'a>, +} + +impl<'a> Iterator for ProtoStackIter<'a> { + type Item = &'static str; + fn next(&mut self) -> Option { + self.parts.next().as_ref().map(Protocol::tag) + } +} + impl<'a> From> for Multiaddr { fn from(p: Protocol<'a>) -> Multiaddr { let mut w = Vec::new(); diff --git a/src/protocol.rs b/src/protocol.rs index 59b9537..7474096 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -510,66 +510,88 @@ impl<'a> Protocol<'a> { Wss(cow) => Wss(Cow::Owned(cow.into_owned())), } } + + pub fn tag(&self) -> &'static str { + use self::Protocol::*; + match self { + Dccp(_) => "dccp", + Dns(_) => "dns", + Dns4(_) => "dns4", + Dns6(_) => "dns6", + Dnsaddr(_) => "dnsaddr", + Http => "http", + Https => "https", + Ip4(_) => "ip4", + Ip6(_) => "ip6", + P2pWebRtcDirect => "p2p-webrtc-direct", + P2pWebRtcStar => "p2p-webrtc-star", + WebRTC => "webrtc", + Certhash(_) => "certhash", + P2pWebSocketStar => "p2p-websocket-star", + Memory(_) => "memory", + Onion(_, _) => "onion", + Onion3(_) => "onion3", + P2p(_) => "p2p", + P2pCircuit => "p2p-circuit", + Quic => "quic", + Sctp(_) => "sctp", + Tcp(_) => "tcp", + Tls => "tls", + Noise => "noise", + Udp(_) => "udp", + Udt => "udt", + Unix(_) => "unix", + Utp => "utp", + Ws(ref s) if s == "/" => "ws", + Ws(_) => "x-parity-ws", + Wss(ref s) if s == "/" => "wss", + Wss(_) => "x-parity-wss", + } + } } impl<'a> fmt::Display for Protocol<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use self::Protocol::*; + write!(f, "/{}", self.tag())?; match self { - Dccp(port) => write!(f, "/dccp/{}", port), - Dns(s) => write!(f, "/dns/{}", s), - Dns4(s) => write!(f, "/dns4/{}", s), - Dns6(s) => write!(f, "/dns6/{}", s), - Dnsaddr(s) => write!(f, "/dnsaddr/{}", s), - Http => f.write_str("/http"), - Https => f.write_str("/https"), - Ip4(addr) => write!(f, "/ip4/{}", addr), - Ip6(addr) => write!(f, "/ip6/{}", addr), - P2pWebRtcDirect => f.write_str("/p2p-webrtc-direct"), - P2pWebRtcStar => f.write_str("/p2p-webrtc-star"), - WebRTC => f.write_str("/webrtc"), + Dccp(port) => write!(f, "/{}", port), + Dns(s) => write!(f, "/{}", s), + Dns4(s) => write!(f, "/{}", s), + Dns6(s) => write!(f, "/{}", s), + Dnsaddr(s) => write!(f, "/{}", s), + Ip4(addr) => write!(f, "/{}", addr), + Ip6(addr) => write!(f, "/{}", addr), Certhash(hash) => write!( f, - "/certhash/{}", + "/{}", multibase::encode(multibase::Base::Base64Url, hash.to_bytes()) ), - P2pWebSocketStar => f.write_str("/p2p-websocket-star"), - Memory(port) => write!(f, "/memory/{}", port), + Memory(port) => write!(f, "/{}", port), Onion(addr, port) => { let s = BASE32.encode(addr.as_ref()); - write!(f, "/onion/{}:{}", s.to_lowercase(), port) + write!(f, "/{}:{}", s.to_lowercase(), port) } Onion3(addr) => { let s = BASE32.encode(addr.hash()); - write!(f, "/onion3/{}:{}", s.to_lowercase(), addr.port()) - } - P2p(c) => write!( - f, - "/p2p/{}", - multibase::Base::Base58Btc.encode(c.to_bytes()) - ), - P2pCircuit => f.write_str("/p2p-circuit"), - Quic => f.write_str("/quic"), - Sctp(port) => write!(f, "/sctp/{}", port), - Tcp(port) => write!(f, "/tcp/{}", port), - Tls => write!(f, "/tls"), - Noise => write!(f, "/noise"), - Udp(port) => write!(f, "/udp/{}", port), - Udt => f.write_str("/udt"), - Unix(s) => write!(f, "/unix/{}", s), - Utp => f.write_str("/utp"), - Ws(ref s) if s == "/" => f.write_str("/ws"), - Ws(s) => { + write!(f, "/{}:{}", s.to_lowercase(), addr.port()) + } + P2p(c) => write!(f, "/{}", multibase::Base::Base58Btc.encode(c.to_bytes())), + Sctp(port) => write!(f, "/{}", port), + Tcp(port) => write!(f, "/{}", port), + Udp(port) => write!(f, "/{}", port), + Unix(s) => write!(f, "/{}", s), + Ws(s) if s != "/" => { let encoded = percent_encoding::percent_encode(s.as_bytes(), PATH_SEGMENT_ENCODE_SET); - write!(f, "/x-parity-ws/{}", encoded) + write!(f, "/{}", encoded) } - Wss(ref s) if s == "/" => f.write_str("/wss"), - Wss(s) => { + Wss(s) if s != "/" => { let encoded = percent_encoding::percent_encode(s.as_bytes(), PATH_SEGMENT_ENCODE_SET); - write!(f, "/x-parity-wss/{}", encoded) + write!(f, "/{}", encoded) } + _ => Ok(()), } } } diff --git a/tests/lib.rs b/tests/lib.rs index f31a955..35d48e4 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -1,3 +1,5 @@ +extern crate core; + use data_encoding::HEXUPPER; use multiaddr::*; use multihash::Multihash; @@ -527,3 +529,79 @@ fn unknown_protocol_string() { }, } } + +#[test] +fn protocol_stack() { + let addresses = [ + "/ip4/0.0.0.0", + "/ip6/::1", + "/ip6/2601:9:4f81:9700:803e:ca65:66e8:c21", + "/udp/0", + "/tcp/0", + "/sctp/0", + "/udp/1234", + "/tcp/1234", + "/sctp/1234", + "/udp/65535", + "/tcp/65535", + "/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC", + "/udp/1234/sctp/1234", + "/udp/1234/udt", + "/udp/1234/utp", + "/tcp/1234/http", + "/tcp/1234/tls/http", + "/tcp/1234/https", + "/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC/tcp/1234", + "/ip4/127.0.0.1/udp/1234", + "/ip4/127.0.0.1/udp/0", + "/ip4/127.0.0.1/tcp/1234", + "/ip4/127.0.0.1/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC", + "/ip4/127.0.0.1/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC/tcp/1234", + "/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/tcp/8000/ws/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC", + "/p2p-webrtc-star/ip4/127.0.0.1/tcp/9090/ws/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC", + "/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/tcp/8000/wss/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC", + "/ip4/127.0.0.1/tcp/9090/p2p-circuit/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC", + "/onion/aaimaq4ygg2iegci:80", + "/dnsaddr/sjc-1.bootstrap.libp2p.io", + "/dnsaddr/sjc-1.bootstrap.libp2p.io/tcp/1234/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", + "/ip4/127.0.0.1/tcp/127/ws", + "/ip4/127.0.0.1/tcp/127/tls", + "/ip4/127.0.0.1/tcp/127/tls/ws", + "/ip4/127.0.0.1/tcp/127/noise", + "/ip4/127.0.0.1/udp/1234/webrtc", + ]; + let argless = std::collections::HashSet::from([ + "http", + "https", + "noise", + "p2p-circuit", + "p2p-webrtc-direct", + "p2p-webrtc-star", + "p2p-websocket-star", + "quic", + "tls", + "udt", + "utp", + "webrtc", + "ws", + "wss", + ]); + for addr_str in addresses { + let ma = Multiaddr::from_str(addr_str).expect("These are supposed to be valid multiaddrs"); + let ps: Vec<&str> = ma.protocol_stack().collect(); + let mut toks: Vec<&str> = addr_str.split('/').collect(); + assert_eq!("", toks[0]); + toks.remove(0); + let mut i = 0; + while i < toks.len() { + let proto_tag = toks[i]; + i += 1; + if argless.contains(proto_tag) { + //skip + } else { + toks.remove(i); + } + } + assert_eq!(ps, toks); + } +}