diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f04bb99..462038c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,7 +46,7 @@ jobs: shell: bash - name: Nightly Test - run: cargo test + run: cargo test --all-features shell: bash env: RUSTFLAGS: '--cfg nightly -Zcrate-attr=feature(variant_count)' diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f035335 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "rust-analyzer.check.features": [ + "arb" + ], + "rust-analyzer.cargo.features": [ + "arb" + ] +} \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 72e1d04..ab46ce5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,12 @@ version = "0.18.3" [features] default = ["url"] +arb = [ + "dep:quickcheck", + "dep:rand", + "multihash/arb", + "dep:strum", +] [dependencies] arrayref = "0.3" @@ -26,11 +32,12 @@ static_assertions = "1.1" unsigned-varint = "0.8" url = { version = "2.5.0", optional = true, default-features = false } libp2p-identity = { version = "0.2.9", features = ["peerid"] } +quickcheck = { version = "1.0.3", default-features = false, optional = true } +rand = {version = "0.9.0", optional = true} +strum = {version = "0.27", features = ["derive"], optional = true} [dev-dependencies] bincode = "1" -quickcheck = { version = "1.0.3", default-features = false } -rand = "0.9.0" serde_json = "1.0" # Passing arguments to the docsrs builder in order to properly document cfg's. diff --git a/src/lib.rs b/src/lib.rs index b6b0ad4..34d0ed7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -223,7 +223,7 @@ impl Multiaddr { /// 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 { + pub fn protocol_stack(&self) -> ProtoStackIter<'_> { ProtoStackIter { parts: self.iter() } } } diff --git a/src/protocol.rs b/src/protocol.rs index f0fa1d4..3b24d8f 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -1,3 +1,8 @@ +#[cfg(feature = "arb")] +mod arb_impl; +#[cfg(feature = "arb")] +pub mod arb_util; + use crate::onion_addr::Onion3Addr; use crate::{Error, PeerId, Result}; use arrayref::array_ref; @@ -84,9 +89,9 @@ const PATH_SEGMENT_ENCODE_SET: &percent_encoding::AsciiSet = &percent_encoding:: /// /// For `Unix`, `Ws` and `Wss` we use `&str` instead of `Path` to allow /// cross-platform usage of `Protocol` since encoding `Paths` to bytes is -/// platform-specific. This means that the actual validation of paths needs to -/// happen separately. +/// platform-specific. This means that the actual validation of paths needs to happen separately. #[derive(PartialEq, Eq, Clone, Debug)] +#[cfg_attr(feature = "arb", derive(strum::EnumCount))] #[non_exhaustive] pub enum Protocol<'a> { Dccp(u16), diff --git a/src/protocol/arb_impl.rs b/src/protocol/arb_impl.rs new file mode 100644 index 0000000..3ac51ff --- /dev/null +++ b/src/protocol/arb_impl.rs @@ -0,0 +1,91 @@ +use std::borrow::Cow; + +use crate::{protocol::arb_util::SubString, Multiaddr, Protocol}; + +impl quickcheck::Arbitrary for Multiaddr { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let iter = (0..u8::arbitrary(g) % 128).map(|_| Protocol::arbitrary(g)); + Multiaddr::from_iter(iter) + } +} + +impl quickcheck::Arbitrary for Protocol<'static> { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + use quickcheck::Arbitrary; + use std::iter; + use strum::EnumCount; + match usize::arbitrary(g) % Protocol::COUNT { + 0 => Protocol::Dccp(Arbitrary::arbitrary(g)), + 1 => Protocol::Dns(Cow::Owned(SubString::arbitrary(g).0)), + 2 => Protocol::Dns4(Cow::Owned(SubString::arbitrary(g).0)), + 3 => Protocol::Dns6(Cow::Owned(SubString::arbitrary(g).0)), + 4 => Protocol::Dnsaddr(Cow::Owned(SubString::arbitrary(g).0)), + 5 => Protocol::Http, + 6 => Protocol::Https, + 7 => Protocol::Ip4(std::net::Ipv4Addr::arbitrary(g)), + 8 => Protocol::Ip6(std::net::Ipv6Addr::arbitrary(g)), + 9 => Protocol::P2pWebRtcDirect, + 10 => Protocol::P2pWebRtcStar, + 11 => Protocol::WebRTCDirect, + 12 => Protocol::Certhash(crate::protocol::Multihash::arbitrary(g)), + 13 => Protocol::P2pWebSocketStar, + 14 => Protocol::Memory(Arbitrary::arbitrary(g)), + 15 => { + let a = iter::repeat_with(|| u8::arbitrary(g)) + .take(10) + .collect::>() + .try_into() + .unwrap(); + Protocol::Onion(Cow::Owned(a), std::cmp::max(1, u16::arbitrary(g))) + } + 16 => { + let a: [u8; 35] = iter::repeat_with(|| u8::arbitrary(g)) + .take(35) + .collect::>() + .try_into() + .unwrap(); + Protocol::Onion3((a, std::cmp::max(1, u16::arbitrary(g))).into()) + } + 17 => Protocol::P2p(crate::protocol::arb_util::PId::arbitrary(g).0), + 18 => Protocol::P2pCircuit, + 19 => Protocol::Quic, + 20 => Protocol::QuicV1, + 21 => Protocol::Sctp(Arbitrary::arbitrary(g)), + 22 => Protocol::Tcp(Arbitrary::arbitrary(g)), + 23 => Protocol::Tls, + 24 => Protocol::Noise, + 25 => Protocol::Udp(Arbitrary::arbitrary(g)), + 26 => Protocol::Udt, + 27 => Protocol::Unix(Cow::Owned(SubString::arbitrary(g).0)), + 28 => Protocol::Utp, + 29 => Protocol::WebTransport, + 30 => Protocol::Ws("/".into()), + 31 => Protocol::Wss("/".into()), + 32 => Protocol::Ip6zone(Cow::Owned(SubString::arbitrary(g).0)), + 33 => Protocol::Ipcidr(Arbitrary::arbitrary(g)), + 34 => { + let len = usize::arbitrary(g) % (462 - 387) + 387; + let a = iter::repeat_with(|| u8::arbitrary(g)) + .take(len) + .collect::>(); + Protocol::Garlic64(Cow::Owned(a)) + } + 35 => { + let len = if bool::arbitrary(g) { + 32 + } else { + usize::arbitrary(g) % 128 + 35 + }; + let a = iter::repeat_with(|| u8::arbitrary(g)) + .take(len) + .collect::>(); + Protocol::Garlic32(Cow::Owned(a)) + } + 36 => Protocol::Sni(Cow::Owned(SubString::arbitrary(g).0)), + 37 => Protocol::P2pStardust, + 38 => Protocol::WebRTC, + 39 => Protocol::HttpPath(Cow::Owned(SubString::arbitrary(g).0)), + _ => panic!("outside range"), + } + } +} diff --git a/src/protocol/arb_util.rs b/src/protocol/arb_util.rs new file mode 100644 index 0000000..8cc6a1b --- /dev/null +++ b/src/protocol/arb_util.rs @@ -0,0 +1,31 @@ +//! helper types + +use libp2p_identity::PeerId; + +/// `PeerId` doesn't `impl quickcheck::Arbitrary` so this newtype is useful beyond this crate for the similar purposes. +#[derive(PartialEq, Eq, Clone, Debug)] +pub struct PId(pub PeerId); + +impl quickcheck::Arbitrary for PId { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let mut hash: [u8; 32] = [0; 32]; + hash.fill_with(|| u8::arbitrary(g)); + + PId(PeerId::from_multihash( + multihash::Multihash::wrap(0x0, &hash).expect("The digest size is never too large"), + ) + .expect("identity multihash works if digest size < 64")) + } +} + +/// ASCII string without '/' +#[derive(PartialEq, Eq, Clone, Debug)] +pub struct SubString(pub String); + +impl quickcheck::Arbitrary for SubString { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let mut s = String::arbitrary(g); + s.retain(|c| c.is_ascii() && c != '/'); + SubString(s) + } +} diff --git a/tests/lib.rs b/tests/lib.rs index 936809e..20148d4 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -1,11 +1,10 @@ use data_encoding::HEXUPPER; use multiaddr::*; use multihash::Multihash; -use quickcheck::{Arbitrary, Gen, QuickCheck}; +use quickcheck::QuickCheck; use std::{ borrow::Cow, - convert::{TryFrom, TryInto}, - iter::{self, FromIterator}, + convert::TryFrom, net::{Ipv4Addr, Ipv6Addr}, str::FromStr, }; @@ -14,51 +13,49 @@ use std::{ #[test] fn to_from_bytes_identity() { - fn prop(a: Ma) -> bool { - let b = a.0.to_vec(); - Some(a) == Multiaddr::try_from(b).ok().map(Ma) + fn prop(a: Multiaddr) -> bool { + let b = a.to_vec(); + Some(a) == Multiaddr::try_from(b).ok() } - QuickCheck::new().quickcheck(prop as fn(Ma) -> bool) + QuickCheck::new().quickcheck(prop as fn(Multiaddr) -> bool) } #[test] fn to_from_str_identity() { - fn prop(a: Ma) -> bool { - let b = a.0.to_string(); - Some(a) == Multiaddr::from_str(&b).ok().map(Ma) + fn prop(a: Multiaddr) -> bool { + let b = a.to_string(); + Some(a) == Multiaddr::from_str(&b).ok() } - QuickCheck::new().quickcheck(prop as fn(Ma) -> bool) + QuickCheck::new().quickcheck(prop as fn(Multiaddr) -> bool) } #[test] fn byteswriter() { - fn prop(a: Ma, b: Ma) -> bool { - let mut x = a.0.clone(); - for p in b.0.iter() { + fn prop(a: Multiaddr, b: Multiaddr) -> bool { + let mut x = a.clone(); + for p in b.iter() { x = x.with(p) } - x.iter() - .zip(a.0.iter().chain(b.0.iter())) - .all(|(x, y)| x == y) + x.iter().zip(a.iter().chain(b.iter())).all(|(x, y)| x == y) } - QuickCheck::new().quickcheck(prop as fn(Ma, Ma) -> bool) + QuickCheck::new().quickcheck(prop as fn(Multiaddr, Multiaddr) -> bool) } #[test] fn push_pop_identity() { - fn prop(a: Ma, p: Proto) -> bool { + fn prop(a: Multiaddr, p: Protocol) -> bool { let mut b = a.clone(); let q = p.clone(); - b.0.push(q.0); - assert_ne!(a.0, b.0); - Some(p.0) == b.0.pop() && a.0 == b.0 + b.push(q); + assert_ne!(a, b); + Some(p) == b.pop() && a == b } - QuickCheck::new().quickcheck(prop as fn(Ma, Proto) -> bool) + QuickCheck::new().quickcheck(prop as fn(Multiaddr, Protocol<'static>) -> bool) } #[test] fn ends_with() { - fn prop(Ma(m): Ma) { + fn prop(m: Multiaddr) { let n = m.iter().count(); for i in 0..n { let suffix = m.iter().skip(i).collect::(); @@ -70,7 +67,7 @@ fn ends_with() { #[test] fn starts_with() { - fn prop(Ma(m): Ma) { + fn prop(m: Multiaddr) { let n = m.iter().count(); for i in 0..n { let prefix = m.iter().take(i + 1).collect::(); @@ -80,137 +77,6 @@ fn starts_with() { QuickCheck::new().quickcheck(prop as fn(_)) } -// Arbitrary impls - -#[derive(PartialEq, Eq, Clone, Hash, Debug)] -struct Ma(Multiaddr); - -impl Arbitrary for Ma { - fn arbitrary(g: &mut Gen) -> Self { - let iter = (0..u8::arbitrary(g) % 128).map(|_| Proto::arbitrary(g).0); - Ma(Multiaddr::from_iter(iter)) - } -} - -#[derive(PartialEq, Eq, Clone, Debug)] -struct Proto(Protocol<'static>); - -impl Proto { - const IMPL_VARIANT_COUNT: u8 = 40; -} - -impl Arbitrary for Proto { - fn arbitrary(g: &mut Gen) -> Self { - use Protocol::*; - match u8::arbitrary(g) % Proto::IMPL_VARIANT_COUNT { - 0 => Proto(Dccp(Arbitrary::arbitrary(g))), - 1 => Proto(Dns(Cow::Owned(SubString::arbitrary(g).0))), - 2 => Proto(Dns4(Cow::Owned(SubString::arbitrary(g).0))), - 3 => Proto(Dns6(Cow::Owned(SubString::arbitrary(g).0))), - 4 => Proto(Dnsaddr(Cow::Owned(SubString::arbitrary(g).0))), - 5 => Proto(Http), - 6 => Proto(Https), - 7 => Proto(Ip4(Ipv4Addr::arbitrary(g))), - 8 => Proto(Ip6(Ipv6Addr::arbitrary(g))), - 9 => Proto(P2pWebRtcDirect), - 10 => Proto(P2pWebRtcStar), - 11 => Proto(WebRTCDirect), - 12 => Proto(Certhash(Mh::arbitrary(g).0)), - 13 => Proto(P2pWebSocketStar), - 14 => Proto(Memory(Arbitrary::arbitrary(g))), - 15 => { - let a = iter::repeat_with(|| u8::arbitrary(g)) - .take(10) - .collect::>() - .try_into() - .unwrap(); - Proto(Onion(Cow::Owned(a), std::cmp::max(1, u16::arbitrary(g)))) - } - 16 => { - let a: [u8; 35] = iter::repeat_with(|| u8::arbitrary(g)) - .take(35) - .collect::>() - .try_into() - .unwrap(); - Proto(Onion3((a, std::cmp::max(1, u16::arbitrary(g))).into())) - } - 17 => Proto(P2p(PId::arbitrary(g).0)), - 18 => Proto(P2pCircuit), - 19 => Proto(Quic), - 20 => Proto(QuicV1), - 21 => Proto(Sctp(Arbitrary::arbitrary(g))), - 22 => Proto(Tcp(Arbitrary::arbitrary(g))), - 23 => Proto(Tls), - 24 => Proto(Noise), - 25 => Proto(Udp(Arbitrary::arbitrary(g))), - 26 => Proto(Udt), - 27 => Proto(Unix(Cow::Owned(SubString::arbitrary(g).0))), - 28 => Proto(Utp), - 29 => Proto(WebTransport), - 30 => Proto(Ws("/".into())), - 31 => Proto(Wss("/".into())), - 32 => Proto(Ip6zone(Cow::Owned(SubString::arbitrary(g).0))), - 33 => Proto(Ipcidr(Arbitrary::arbitrary(g))), - 34 => { - let len = usize::arbitrary(g) % (462 - 387) + 387; - let a = iter::repeat_with(|| u8::arbitrary(g)) - .take(len) - .collect::>(); - Proto(Garlic64(Cow::Owned(a))) - } - 35 => { - let len = if bool::arbitrary(g) { - 32 - } else { - usize::arbitrary(g) % 128 + 35 - }; - let a = iter::repeat_with(|| u8::arbitrary(g)) - .take(len) - .collect::>(); - Proto(Garlic32(Cow::Owned(a))) - } - 36 => Proto(Sni(Cow::Owned(SubString::arbitrary(g).0))), - 37 => Proto(P2pStardust), - 38 => Proto(WebRTC), - 39 => Proto(HttpPath(Cow::Owned(SubString::arbitrary(g).0))), - _ => panic!("outside range"), - } - } -} - -#[derive(Clone, Debug)] -struct Mh(Multihash<64>); - -impl Arbitrary for Mh { - fn arbitrary(g: &mut Gen) -> Self { - let mut hash: [u8; 32] = [0; 32]; - hash.fill_with(|| u8::arbitrary(g)); - Mh(Multihash::wrap(0x0, &hash).expect("The digest size is never too large")) - } -} - -#[derive(Clone, Debug)] -struct PId(PeerId); - -impl Arbitrary for PId { - fn arbitrary(g: &mut Gen) -> Self { - let mh = Mh::arbitrary(g); - - PId(PeerId::from_multihash(mh.0).expect("identity multihash works if digest size < 64")) - } -} - -#[derive(PartialEq, Eq, Clone, Debug)] -struct SubString(String); // ASCII string without '/' - -impl Arbitrary for SubString { - fn arbitrary(g: &mut Gen) -> Self { - let mut s = String::arbitrary(g); - s.retain(|c| c.is_ascii() && c != '/'); - SubString(s) - } -} - // other unit tests fn ma_valid(source: &str, target: &str, protocols: Vec>) { @@ -761,15 +627,6 @@ fn protocol_stack() { } } -// Assert all `Protocol` variants are covered -// in its `Arbitrary` impl. -#[cfg(nightly)] -#[test] -fn arbitrary_impl_for_all_proto_variants() { - let variants = core::mem::variant_count::() as u8; - assert_eq!(variants, Proto::IMPL_VARIANT_COUNT); -} - mod multiaddr_with_p2p { use multiaddr::{Multiaddr, PeerId};