diff --git a/build.sbt b/build.sbt index 73f3df9..143aef2 100644 --- a/build.sbt +++ b/build.sbt @@ -33,7 +33,9 @@ ThisBuild / mimaBinaryIssueFilters ++= Seq( ProblemFilters.exclude[MissingTypesProblem]("com.comcast.ip4s.Multicast$"), ProblemFilters.exclude[DirectMissingMethodProblem]("com.comcast.ip4s.SourceSpecificMulticast.ordinal"), ProblemFilters.exclude[MissingTypesProblem]("com.comcast.ip4s.SourceSpecificMulticast$"), - ProblemFilters.exclude[ReversedMissingMethodProblem]("com.comcast.ip4s.IpAddress.isPrivate") // #562 + ProblemFilters.exclude[ReversedMissingMethodProblem]("com.comcast.ip4s.IpAddress.isPrivate"), // #562 + ProblemFilters.exclude[ReversedMissingMethodProblem]("com.comcast.ip4s.IpAddress.isLoopback"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("com.comcast.ip4s.IpAddress.isLinkLocal") ) lazy val root = tlCrossRootProject.aggregate(core, testKit) diff --git a/shared/src/main/scala/com/comcast/ip4s/Host.scala b/shared/src/main/scala/com/comcast/ip4s/Host.scala index e0680fa..88cd7b1 100644 --- a/shared/src/main/scala/com/comcast/ip4s/Host.scala +++ b/shared/src/main/scala/com/comcast/ip4s/Host.scala @@ -215,6 +215,12 @@ sealed abstract class IpAddress extends IpAddressPlatform with Host with Seriali def asSourceSpecificMulticastLenient: Option[SourceSpecificMulticast[this.type]] = SourceSpecificMulticast.fromIpAddressLenient(this) + /** Returns true if this address is a loopback address. */ + def isLoopback: Boolean + + /** Returns true if this address is a link local address. */ + def isLinkLocal: Boolean + /** Returns true if this address is in a private range. */ def isPrivate: Boolean @@ -359,6 +365,12 @@ final class Ipv4Address private (protected val bytes: Array[Byte]) extends IpAdd override def isSourceSpecificMulticast: Boolean = this >= Ipv4Address.SourceSpecificMulticastRangeStart && this <= Ipv4Address.SourceSpecificMulticastRangeEnd + override def isLoopback: Boolean = + Ipv4Address.Classes.Loopback.contains(this) + + override def isLinkLocal: Boolean = + Ipv4Address.Classes.LinkLocal.contains(this) + override def isPrivate: Boolean = Ipv4Address.Classes.Private.A.contains(this) || Ipv4Address.Classes.Private.B.contains(this) || @@ -456,6 +468,12 @@ object Ipv4Address extends Ipv4AddressCompanionPlatform { /** Class A: 192.168.0.0 - 192.168.255.255 */ val C: Cidr[Ipv4Address] = Cidr(fromBytes(192, 168, 0, 0), 16) } + + /** Loopback: 127.0.0.0 - 127.255.255.255. */ + val Loopback: Cidr[Ipv4Address] = Cidr(fromBytes(127, 0, 0, 0), 8) + + /** Link local: 169.254.0.0 - 169.254.255.255. */ + val LinkLocal: Cidr[Ipv4Address] = Cidr(fromBytes(169, 254, 0, 0), 16) } /** Parses an IPv4 address from a dotted-decimal string, returning `None` if the string is not a valid IPv4 address. @@ -649,6 +667,12 @@ final class Ipv6Address private (protected val bytes: Array[Byte]) extends IpAdd override def isSourceSpecificMulticast: Boolean = this >= Ipv6Address.SourceSpecificMulticastRangeStart && this <= Ipv6Address.SourceSpecificMulticastRangeEnd + override def isLoopback: Boolean = + this == Ipv6Address.Loopback || (isMappedV4 && collapseMappedV4.isLoopback) + + override def isLinkLocal: Boolean = + Ipv6Address.LinkLocalBlock.contains(this) || (isMappedV4 && collapseMappedV4.isLinkLocal) + override def isPrivate: Boolean = Ipv6Address.UniqueLocalBlock.contains(this) || (isMappedV4 && collapseMappedV4.isPrivate) @@ -696,10 +720,17 @@ object Ipv6Address extends Ipv6AddressCompanionPlatform { val MappedV4Block: Cidr[Ipv6Address] = Cidr(Ipv6Address.fromBytes(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0), 96) - /** CIDR which defines unique local address block. */ + /** Alias for ::1. */ + val Loopback: Ipv6Address = Ipv6Address.fromBytes(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1) + + /** CIDR which defines the unique local address block. */ val UniqueLocalBlock: Cidr[Ipv6Address] = Cidr(Ipv6Address.fromBytes(0xfc, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), 7) + /** CIDR which defines the linked scope unicast address block. */ + val LinkLocalBlock: Cidr[Ipv6Address] = + Cidr(Ipv6Address.fromBytes(0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), 10) + /** Parses an IPv6 address from a string in RFC4291 notation, returning `None` if the string is not a valid IPv6 * address. */ diff --git a/test-kit/shared/src/test/scala/com/comcast/ip4s/Ipv4AddressTest.scala b/test-kit/shared/src/test/scala/com/comcast/ip4s/Ipv4AddressTest.scala index a190ff8..a360381 100644 --- a/test-kit/shared/src/test/scala/com/comcast/ip4s/Ipv4AddressTest.scala +++ b/test-kit/shared/src/test/scala/com/comcast/ip4s/Ipv4AddressTest.scala @@ -84,4 +84,17 @@ class Ipv4AddressTest extends BaseTestSuite { assert(ipv4"192.168.255.255".isPrivate) assert(!ipv4"192.168.255.255".next.isPrivate) } + + test("isLoopback") { + assert(ipv4"127.0.0.1".isLoopback) + assert(ipv4"127.255.255.255".isLoopback) + assert(!ipv4"128.0.0.0".isLoopback) + } + + test("isLinkLocal") { + assert(!ipv4"127.0.0.1".isLinkLocal) + assert(ipv4"169.254.0.0".isLinkLocal) + assert(ipv4"169.254.255.255".isLinkLocal) + assert(!ipv4"169.254.255.255".next.isLinkLocal) + } } diff --git a/test-kit/shared/src/test/scala/com/comcast/ip4s/Ipv6AddressTest.scala b/test-kit/shared/src/test/scala/com/comcast/ip4s/Ipv6AddressTest.scala index 4d845f7..476786a 100644 --- a/test-kit/shared/src/test/scala/com/comcast/ip4s/Ipv6AddressTest.scala +++ b/test-kit/shared/src/test/scala/com/comcast/ip4s/Ipv6AddressTest.scala @@ -140,4 +140,14 @@ class Ipv6AddressTest extends BaseTestSuite { // mapped v4 assert(ipv6"::ffff:10.1.1.1".isPrivate) } + + test("isLoopback") { + assert(ipv6"::1".isLoopback) + assert(ipv6"::ffff:127.0.0.1".isLoopback) + } + + test("isLinkLocal") { + assert(ipv6"fe80::1".isLinkLocal) + assert(ipv6"::ffff:169.254.0.0".isLinkLocal) + } }