All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.comcast.ip4s.Host.scala Maven / Gradle / Ivy

There is a newer version: 3.6.0
Show newest version
/*
 * Copyright 2018 Comcast Cable Communications Management, LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.comcast.ip4s

import cats.{Applicative, ApplicativeThrow, Order, Show}
import cats.syntax.all._

import scala.util.hashing.MurmurHash3

// annoying bincompat shim
private[ip4s] trait HostPlatform

/** ADT representing either an `IpAddress`, `Hostname`, or `IDN`. */
sealed trait Host extends HostPlatform with Ordered[Host] {

  def compare(that: Host): Int =
    this match {
      case x: Ipv4Address =>
        that match {
          case y: Ipv4Address => IpAddress.compareBytes(x, y)
          case y: Ipv6Address => IpAddress.compareBytes(x.toCompatV6, y)
          case _              => -1
        }
      case x: Ipv6Address =>
        that match {
          case y: Ipv4Address => IpAddress.compareBytes(x, y.toCompatV6)
          case y: Ipv6Address => IpAddress.compareBytes(x, y)
          case _              => -1
        }
      case x: Hostname =>
        that match {
          case _: Ipv4Address => 1
          case _: Ipv6Address => 1
          case y: Hostname    => x.toString.compare(y.toString)
          case y: IDN         => x.toString.compare(y.hostname.toString)
        }
      case x: IDN =>
        that match {
          case _: Ipv4Address => 1
          case _: Ipv6Address => 1
          case y: Hostname    => x.hostname.toString.compare(y.toString)
          case y: IDN         => x.hostname.toString.compare(y.hostname.toString)
        }
    }

  /** Resolves this host to an ip address using the platform DNS resolver.
    *
    * If the host cannot be resolved, the effect fails with a `com.comcast.ip4s.UnknownHostException`.
    */
  def resolve[F[_]: Dns: Applicative]: F[IpAddress] =
    this match {
      case ip: IpAddress      => Applicative[F].pure(ip)
      case hostname: Hostname => Dns[F].resolve(hostname)
      case idn: IDN           => Dns[F].resolve(idn.hostname)
    }

  /** Resolves this host to an ip address using the platform DNS resolver.
    *
    * If the host cannot be resolved, a `None` is returned.
    */
  def resolveOption[F[_]: Dns: ApplicativeThrow]: F[Option[IpAddress]] =
    resolve[F].map(Some(_): Option[IpAddress]).recover { case _: UnknownHostException => None }

  /** Resolves this host to all ip addresses known to the platform DNS resolver.
    *
    * If the host cannot be resolved, an empty list is returned.
    */
  def resolveAll[F[_]: Dns: Applicative]: F[List[IpAddress]] =
    this match {
      case ip: IpAddress      => Applicative[F].pure(List(ip))
      case hostname: Hostname => Dns[F].resolveAll(hostname)
      case idn: IDN           => Dns[F].resolveAll(idn.hostname)
    }
}

object Host {
  def fromString(string: String): Option[Host] =
    IpAddress.fromString(string) orElse Hostname.fromString(string) orElse IDN.fromString(string)

  implicit def show: Show[Host] = Show.fromToString[Host]
  implicit def order: Order[Host] = Order.fromComparable[Host]
  implicit def ordering: Ordering[Host] = _.compare(_)
}

/** RFC1123 compliant hostname.
  *
  * A hostname contains one or more labels, where each label consists of letters A-Z, a-z, digits 0-9, or a dash. A
  * label may not start or end in a dash and may not exceed 63 characters in length. Labels are separated by periods and
  * the overall hostname must not exceed 253 characters in length.
  */
final class Hostname private (val labels: List[Hostname.Label], override val toString: String) extends Host {

  /** Converts this hostname to lower case. */
  def normalized: Hostname =
    new Hostname(labels.map(l => new Hostname.Label(l.toString.toLowerCase)), toString.toLowerCase)

  override def hashCode: Int = MurmurHash3.stringHash(toString, "Hostname".hashCode)
  override def equals(other: Any): Boolean =
    other match {
      case that: Hostname => toString == that.toString
      case _              => false
    }
}

object Hostname {

  /** Label component of a hostname.
    *
    * A label consists of letters A-Z, a-z, digits 0-9, or a dash. A label may not start or end in a dash and may not
    * exceed 63 characters in length.
    */
  final class Label private[Hostname] (override val toString: String) extends Serializable with Ordered[Label] {
    def compare(that: Label): Int = toString.compare(that.toString)
    override def hashCode: Int = MurmurHash3.stringHash(toString, "Label".hashCode)
    override def equals(other: Any): Boolean =
      other match {
        case that: Label => toString == that.toString
        case _           => false
      }
  }

  private val Pattern =
    """[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*""".r

  /** Constructs a `Hostname` from a string. */
  def fromString(value: String): Option[Hostname] =
    value.size match {
      case 0            => None
      case i if i > 253 => None
      case _ =>
        value match {
          case Pattern(_*) =>
            val labels = value
              .split('.')
              .iterator
              .map(new Label(_))
              .toList
            if (labels.isEmpty) None else Option(new Hostname(labels, value))
          case _ => None
        }
    }
}

sealed trait IpVersion
object IpVersion {
  case object V4 extends IpVersion
  case object V6 extends IpVersion
}

/** Immutable and safe representation of an IP address, either V4 or V6.
  *
  * ===Construction===
  *
  * `IpAddress` instances are constructed in a few different ways:
  *   - via `IpAddress("127.0.0.1")`, which parses a string representation of the IP and returns an `Option[IpAddress]`
  *   - via `IpAddress.fromBytes(arr)`, which returns an IP address if the supplied array is either exactly 4 bytes or
  *     exactly 16 bytes
  *   - via literal syntax like `ip"127.0.0.1"`, which returns an `IpAddress` and fails to *compile* if the IP is
  *     invalid.
  *
  * ===Type Hierarchy===
  *
  * There are two subtypes of `IpAddress` -- [[Ipv4Address]] and [[Ipv6Address]]. Each of these subtypes have a richer
  * API than `IpAddress` and it is often useful to use those types directly, for example if your use case requires a V6
  * address. It is safe to pattern match on `IpAddress` to access `Ipv4Address` or `Ipv6Address` directly, or
  * alternatively, you can use [[fold]].
  *
  * ===JVM Specific API===
  *
  * If using `IpAddress` on the JVM, you can call `toInetAddress` to convert the address to a `java.net.InetAddress`,
  * for use with networking libraries. This method does not exist on the Scala.js version.
  */
sealed abstract class IpAddress extends IpAddressPlatform with Host with Serializable {
  protected val bytes: Array[Byte]

  /** Converts this address to a network order byte array of either 4 or 16 bytes. */
  def toBytes: Array[Byte] = bytes.clone

  /** Converts this address to a value of type `A` using the supplied functions. */
  def fold[A](v4: Ipv4Address => A, v6: Ipv6Address => A): A

  /** Maps a type-preserving function across this IP address. */
  def transform(v4: Ipv4Address => Ipv4Address, v6: Ipv6Address => Ipv6Address): this.type

  /** Returns true if this address is in the multicast range. */
  def isMulticast: Boolean

  /** Converts this address to a multicast address, as long as it is in the multicast address range. */
  def asMulticast: Option[Multicast[this.type]] = Multicast.fromIpAddress(this)

  /** Returns true if this address is in the source specific multicast range. */
  def isSourceSpecificMulticast: Boolean

  /** Converts this address to a source specific multicast address, as long as it is in the source specific multicast
    * address range.
    */
  def asSourceSpecificMulticast: Option[SourceSpecificMulticast.Strict[this.type]] =
    SourceSpecificMulticast.fromIpAddress(this)

  /** Like `asSourceSpecificMulticast` but allows multicast addresses outside the source specific range. */
  def asSourceSpecificMulticastLenient: Option[SourceSpecificMulticast[this.type]] =
    SourceSpecificMulticast.fromIpAddressLenient(this)

  /** Narrows this address to an Ipv4Address if that is the underlying type. */
  def asIpv4: Option[Ipv4Address] = collapseMappedV4.fold(Some(_), _ => None)

  /** Narrows this address to an Ipv6Address if that is the underlying type. */
  def asIpv6: Option[Ipv6Address] = fold(_ => None, Some(_))

  /** Returns the version of this address. */
  def version: IpVersion = fold(_ => IpVersion.V4, _ => IpVersion.V6)

  /** Returns true if this address is a V6 address containing a mapped V4 address. */
  def isMappedV4: Boolean = fold(_ => false, Ipv6Address.MappedV4Block.contains)

  /** If this address is an IPv4 mapped IPv6 address, converts to an IPv4 address, otherwise returns this. */
  def collapseMappedV4: IpAddress =
    fold(identity, v6 => if (v6.isMappedV4) IpAddress.fromBytes(v6.toBytes.takeRight(4)).get else v6)

  /** Constructs a [[Cidr]] address from this address. */
  def /(prefixBits: Int): Cidr[this.type] = Cidr(this, prefixBits)

  /** Returns the number of leading ones in this address. When this address is a netmask, the result can be used to
    * create a CIDR.
    *
    * @example {{{
    * scala> val address = ipv4"192.168.1.25"
    * scala> val netmask = ipv4"255.255.0.0"
    * scala> (address / netmask.prefixBits): Cidr[Ipv4Address]
    * res0: Cidr[Ipv4Address] = 192.168.1.25/16
    * }}}
    */
  def prefixBits: Int = {
    import java.lang.Long.numberOfLeadingZeros
    fold(
      m => numberOfLeadingZeros(~(m.toLong | 0xffffffff00000000L)) - 32,
      m => {
        val upper = (m.toBigInt >> 64).toLong
        val upperNum = numberOfLeadingZeros(~upper)
        if (upperNum == 64) upperNum + numberOfLeadingZeros(~(m.toBigInt.toLong)) else upperNum
      }
    )
  }

  /** Gets the IP address after this address, with overflow from the maximum value to the minimum value. */
  def next: IpAddress

  /** Gets the IP address before this address, with underflow from minimum value to the maximum value. */
  def previous: IpAddress

  /** Converts this address to a string form that is compatible for use in a URI per RFC3986 (namely, IPv6 addresses are
    * rendered in condensed form and surrounded by brackets).
    */
  def toUriString: String

  override def equals(other: Any): Boolean =
    other match {
      case that: IpAddress => java.util.Arrays.equals(bytes, that.bytes)
      case _               => false
    }

  override def hashCode: Int = java.util.Arrays.hashCode(bytes)
}

object IpAddress extends IpAddressCompanionPlatform {

  /** Parses an IP address from a string, either in dotted decimal notation or in RFC4291 notation. */
  def fromString(value: String): Option[IpAddress] =
    Ipv4Address.fromString(value) orElse Ipv6Address.fromString(value)

  /** Constructs an IP address from either a 4-element byte array or a 16-element byte array. Any other size array
    * results in a `None`.
    */
  def fromBytes(bytes: Array[Byte]): Option[IpAddress] =
    Ipv4Address.fromBytes(bytes) orElse Ipv6Address.fromBytes(bytes)

  /** Gets an IP address repesenting the loopback interface. */
  def loopback[F[_]](implicit F: Dns[F]): F[IpAddress] =
    F.loopback

  private[ip4s] def compareBytes(x: IpAddress, y: IpAddress): Int = {
    var i, result = 0
    val xb = x.bytes
    val yb = y.bytes
    val sz = xb.length
    while (i < sz && result == 0) {
      result = Integer.compare(xb(i) & 0xff, yb(i) & 0xff)
      i += 1
    }
    result
  }

  implicit def order[A <: IpAddress]: Order[A] = Order.fromOrdering(IpAddress.ordering[A])
  implicit def ordering[A <: IpAddress]: Ordering[A] = _.compare(_)
}

/** Representation of an IPv4 address that works on both the JVM and Scala.js. */
final class Ipv4Address private (protected val bytes: Array[Byte]) extends IpAddress with Ipv4AddressPlatform {
  override def fold[A](v4: Ipv4Address => A, v6: Ipv6Address => A): A = v4(this)

  override def transform(v4: Ipv4Address => Ipv4Address, v6: Ipv6Address => Ipv6Address): this.type =
    v4(this).asInstanceOf[this.type]

  /** Returns the dotted decimal representation of this address. */
  override def toString: String =
    s"${bytes(0) & 0xff}.${bytes(1) & 0xff}.${bytes(2) & 0xff}.${bytes(3) & 0xff}"

  override def toUriString: String = toString

  /** Gets the IPv4 address after this address, with overflow from `255.255.255.255` to `0.0.0.0`. */
  override def next: Ipv4Address = Ipv4Address.fromLong(toLong + 1)

  /** Gets the IPv4 address before this address, with underflow from `0.0.0.0` to `255.255.255.255`. */
  override def previous: Ipv4Address = Ipv4Address.fromLong(toLong - 1)

  /** Converts this address to a 32-bit unsigned integer. */
  def toLong: Long = {
    val bs = bytes
    var result = 0L
    for (i <- 0 until bs.size) {
      result = (result << 8) | (0x0ff & bs(i))
    }
    result
  }

  override def isMulticast: Boolean =
    this >= Ipv4Address.MulticastRangeStart && this <= Ipv4Address.MulticastRangeEnd

  override def isSourceSpecificMulticast: Boolean =
    this >= Ipv4Address.SourceSpecificMulticastRangeStart && this <= Ipv4Address.SourceSpecificMulticastRangeEnd

  /** Converts this V4 address to a compat V6 address, where the first 12 bytes are all zero and the last 4 bytes
    * contain the bytes of this V4 address.
    */
  def toCompatV6: Ipv6Address = {
    val compat = new Array[Byte](16)
    compat(12) = bytes(0)
    compat(13) = bytes(1)
    compat(14) = bytes(2)
    compat(15) = bytes(3)
    Ipv6Address.fromBytes(compat).get
  }

  /** Converts this V4 address to a mapped V6 address, where the first 10 bytes are all zero, the next two bytes are
    * `ff`, and the last 4 bytes contain the bytes of this V4 address.
    */
  def toMappedV6: Ipv6Address = {
    val mapped = new Array[Byte](16)
    mapped(10) = 255.toByte
    mapped(11) = 255.toByte
    mapped(12) = bytes(0)
    mapped(13) = bytes(1)
    mapped(14) = bytes(2)
    mapped(15) = bytes(3)
    Ipv6Address.fromBytes(mapped).get
  }

  /** Applies the supplied mask to this address.
    *
    * @example {{{
    * scala> ipv4"192.168.29.1".masked(ipv4"255.255.0.0")
    * res0: Ipv4Address = 192.168.0.0
    * }}}
    */
  def masked(mask: Ipv4Address): Ipv4Address =
    Ipv4Address.fromLong(toLong & mask.toLong)

  /** Computes the last address in the network identified by applying the supplied mask to this address.
    *
    * @example {{{
    * scala> ipv4"192.168.29.1".maskedLast(ipv4"255.255.0.0")
    * res0: Ipv4Address = 192.168.255.255
    * }}}
    */
  def maskedLast(mask: Ipv4Address): Ipv4Address =
    Ipv4Address.fromLong(toLong & mask.toLong | ~mask.toLong)
}

object Ipv4Address extends Ipv4AddressCompanionPlatform {

  /** First IP address in the IPv4 multicast range. */
  val MulticastRangeStart: Ipv4Address = fromBytes(224, 0, 0, 0)

  /** Last IP address in the IPv4 multicast range. */
  val MulticastRangeEnd: Ipv4Address = fromBytes(239, 255, 255, 255)

  /** First IP address in the IPv4 source specific multicast range. */
  val SourceSpecificMulticastRangeStart: Ipv4Address = fromBytes(232, 0, 0, 0)

  /** Last IP address in the IPv4 source specific multicast range. */
  val SourceSpecificMulticastRangeEnd: Ipv4Address =
    fromBytes(232, 255, 255, 255)

  /** Parses an IPv4 address from a dotted-decimal string, returning `None` if the string is not a valid IPv4 address.
    */
  def fromString(value: String): Option[Ipv4Address] = {
    val trimmed = value.trim
    val fields = trimmed.split('.')
    if (fields.length == 4) {
      val bytes = new Array[Byte](4)
      var idx = 0
      var result: Option[Ipv4Address] = null
      while (idx < bytes.length && (result eq null)) {
        try {
          val value = fields(idx).toInt
          if (value >= 0 && value < 256) bytes(idx) = value.toByte
          else result = None
          idx += 1
        } catch {
          case _: NumberFormatException =>
            result = None
        }
      }
      if (result eq null) Some(unsafeFromBytes(bytes)) else result
    } else None
  }

  /** Constructs an IPv4 address from a 4-element byte array. Returns `Some` when array is exactly 4-bytes and `None`
    * otherwise.
    */
  def fromBytes(bytes: Array[Byte]): Option[Ipv4Address] =
    if (bytes.size == 4) Some(unsafeFromBytes(bytes.clone))
    else None

  private def unsafeFromBytes(bytes: Array[Byte]): Ipv4Address =
    new Ipv4Address(bytes)

  /** Constructs an address from the specified 4 bytes.
    *
    * Each byte is represented as an `Int` to avoid having to manually call `.toByte` on each value -- the `toByte` call
    * is done inside this function.
    */
  def fromBytes(a: Int, b: Int, c: Int, d: Int): Ipv4Address = {
    val bytes = new Array[Byte](4)
    bytes(0) = a.toByte
    bytes(1) = b.toByte
    bytes(2) = c.toByte
    bytes(3) = d.toByte
    unsafeFromBytes(bytes)
  }

  /** Constructs an IPv4 address from a `Long`, using the lower 32-bits. */
  def fromLong(value: Long): Ipv4Address = {
    val bytes = new Array[Byte](4)
    var rem = value
    for (i <- 3 to 0 by -1) {
      bytes(i) = (rem & 0x0ff).toByte
      rem = rem >> 8
    }
    unsafeFromBytes(bytes)
  }

  /** Computes a mask by setting the first / left-most `n` bits high.
    *
    * @example {{{
    * scala> Ipv4Address.mask(16)
    * res0: Ipv4Address = 255.255.0.0
    * }}}
    */
  def mask(bits: Int): Ipv4Address = {
    val b = if (bits < 0) 0 else if (bits > 32) 32 else bits
    Ipv4Address.fromLong(if (b == 32) -1L else ~(-1 >>> b).toLong)
  }
}

/** Representation of an IPv6 address that works on both the JVM and Scala.js. */
final class Ipv6Address private (protected val bytes: Array[Byte]) extends IpAddress with Ipv6AddressPlatform {
  override def fold[A](v4: Ipv4Address => A, v6: Ipv6Address => A): A = v6(this)
  override def transform(v4: Ipv4Address => Ipv4Address, v6: Ipv6Address => Ipv6Address): this.type =
    v6(this).asInstanceOf[this.type]

  /** Returns the condensed string representation of the array per RFC5952. */
  override def toString: String = {
    val fields: Array[Int] = new Array[Int](8)
    var condensing = false
    var condensedStart, maxCondensedStart = -1
    var condensedLength, maxCondensedLength = 0
    var idx = 0
    while (idx < 8) {
      val j = 2 * idx
      val field = ((0x0ff & bytes(j)) << 8) | (0x0ff & bytes(j + 1))
      fields(idx) = field
      if (field == 0) {
        if (!condensing) {
          condensing = true
          condensedStart = idx
          condensedLength = 0
        }
        condensedLength += 1
      } else {
        condensing = false
      }
      if (condensedLength > maxCondensedLength) {
        maxCondensedLength = condensedLength
        maxCondensedStart = condensedStart
      }
      idx += 1
    }
    if (maxCondensedLength == 1) maxCondensedStart = -1
    val str = new StringBuilder
    idx = 0
    while (idx < 8) {
      if (idx == maxCondensedStart) {
        str.append("::")
        idx += maxCondensedLength
      } else {
        val hextet = Integer.toString(fields(idx), 16)
        str.append(hextet)
        idx += 1
        if (idx < 8 && idx != maxCondensedStart) str.append(":")
      }
    }
    str.toString
  }

  /** Returns an uncondensed string representation of the array. */
  def toUncondensedString: String = {
    val str = new StringBuilder
    var idx = 0
    val bytes = toBytes
    while (idx < 16) {
      val field = ((bytes(idx) & 0xff) << 8) | (bytes(idx + 1) & 0xff)
      val hextet = f"$field%04x"
      str.append(hextet)
      idx += 2
      if (idx < 15) str.append(":")
    }
    str.toString
  }

  /** Converts this address to a string of form `x:x:x:x:x:x:a.b.c.d` where each `x` represents 16-bits and `a.b.c.d` is
    * IPv4 dotted decimal notation. Consecutive 0 `x` fields are condensed with `::`.
    *
    * For example, the IPv4 address `127.0.0.1` can be converted to a compatible IPv6 address via
    * [[Ipv4Address#toCompatV6]], which is represented as the string `::7f00:1` and the mixed string `::127.0.0.1`.
    *
    * Similarly, `127.0.0.1` can be converted to a mapped V6 address via [[Ipv4Address#toMappedV6]], resulting in
    * `::ffff:7f00:1` and the mixed string `::ffff:127.0.0.1`.
    *
    * This format is described in RFC4291 section 2.2.3.
    *
    * @example {{{
    * scala> ipv6"::7f00:1".toMixedString
    * res0: String = ::127.0.0.1
    * scala> ipv6"ff3b:1234::ffab:7f00:1".toMixedString
    * res0: String = ff3b:1234::ffab:127.0.0.1
    * }}}
    */
  def toMixedString: String = {
    val bytes = toBytes
    val v4 = Ipv4Address.fromBytes(bytes.slice(12, 16)).get
    bytes(12) = 0
    bytes(13) = 1
    bytes(14) = 0
    bytes(15) = 1
    val s = Ipv6Address.unsafeFromBytes(bytes).toString
    val prefix = s.slice(0, s.size - 3)
    prefix + v4.toString
  }

  override def toUriString: String = s"[$toString]"

  /** Gets the IPv6 address after this address, with overflow from `ffff:ffff:....:ffff` to `::`. */
  override def next: Ipv6Address = Ipv6Address.fromBigInt(toBigInt + 1)

  /** Gets the IPv6 address before this address, with underflow from `::` to `ffff:ffff:....:ffff`. */
  override def previous: Ipv6Address = Ipv6Address.fromBigInt(toBigInt - 1)

  /** Converts this address to a 128-bit unsigned integer. */
  def toBigInt: BigInt = {
    val bs = bytes
    var result = BigInt(0)
    for (i <- 0 until bs.size) {
      result = (result << 8) | (0x0ff & bs(i))
    }
    result
  }

  override def isMulticast: Boolean =
    this >= Ipv6Address.MulticastRangeStart && this <= Ipv6Address.MulticastRangeEnd

  override def isSourceSpecificMulticast: Boolean =
    this >= Ipv6Address.SourceSpecificMulticastRangeStart && this <= Ipv6Address.SourceSpecificMulticastRangeEnd

  /** Applies the supplied mask to this address.
    *
    * @example {{{
    * scala> ipv6"ff3b::1".masked(ipv6"fff0::")
    * res0: Ipv6Address = ff30::
    * }}}
    */
  def masked(mask: Ipv6Address): Ipv6Address =
    Ipv6Address.fromBigInt(toBigInt & mask.toBigInt)

  /** Computes the last address in the network identified by applying the supplied mask to this address.
    *
    * @example {{{
    * scala> ipv6"ff3b::1".maskedLast(ipv6"fff0::")
    * res0: Ipv6Address = ff3f:ffff:ffff:ffff:ffff:ffff:ffff:ffff
    * }}}
    */
  def maskedLast(mask: Ipv6Address): Ipv6Address =
    Ipv6Address.fromBigInt(toBigInt & mask.toBigInt | ~mask.toBigInt)
}

object Ipv6Address extends Ipv6AddressCompanionPlatform {

  /** First IP address in the IPv6 multicast range. */
  val MulticastRangeStart: Ipv6Address =
    fromBytes(255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)

  /** Last IP address in the IPv6 multicast range. */
  val MulticastRangeEnd: Ipv6Address =
    fromBytes(255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255)

  /** First IP address in the IPv6 source specific multicast range. */
  val SourceSpecificMulticastRangeStart: Ipv6Address =
    fromBytes(255, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)

  /** Last IP address in the IPv6 source specific multicast range. */
  val SourceSpecificMulticastRangeEnd: Ipv6Address =
    fromBytes(255, 63, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255)

  /** CIDR which defines the mapped IPv4 address block (https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.2).
    */
  val MappedV4Block: Cidr[Ipv6Address] =
    Cidr(Ipv6Address.fromBytes(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0), 96)

  /** Parses an IPv6 address from a string in RFC4291 notation, returning `None` if the string is not a valid IPv6
    * address.
    */
  def fromString(value: String): Option[Ipv6Address] =
    fromNonMixedString(value) orElse fromMixedString(value)

  private def fromNonMixedString(value: String): Option[Ipv6Address] = {
    var prefix: List[Int] = Nil
    var beforeCondenser = true
    var suffix: List[Int] = Nil
    val trimmed = value.trim()
    var result: Option[Ipv6Address] = null
    val fields =
      if (trimmed.contains(':')) trimmed.split(':')
      else Array.empty[String]
    // if (trimmed.nonEmpty) trimmed.split(':') else Array.empty[String]
    var idx = 0
    while (idx < fields.size && (result eq null)) {
      val field = fields(idx)
      if (field.isEmpty) {
        if (beforeCondenser) {
          beforeCondenser = false
          if (idx + 1 < fields.size && fields(idx + 1).isEmpty) idx += 1
        } else {
          result = None
        }
      } else {
        try {
          if (field.size > 4) {
            result = None
          } else {
            val fieldValue = Integer.parseInt(field, 16)
            if (beforeCondenser) prefix = fieldValue :: prefix
            else suffix = fieldValue :: suffix
          }
        } catch {
          case _: NumberFormatException =>
            result = None
        }
      }
      idx += 1
    }
    if (result ne null) {
      result
    } else if (fields.isEmpty && (trimmed.isEmpty || trimmed != "::")) {
      None
    } else {
      val bytes = new Array[Byte](16)
      idx = 0
      val prefixSize = prefix.size
      var prefixIdx = prefixSize - 1
      while (prefixIdx >= 0) {
        val value = prefix(prefixIdx)
        bytes(idx) = (value >> 8).toByte
        bytes(idx + 1) = value.toByte
        prefixIdx -= 1
        idx += 2
      }
      val suffixSize = suffix.size
      val numCondensedZeroes = bytes.size - idx - (suffixSize * 2)
      idx += numCondensedZeroes
      var suffixIdx = suffixSize - 1
      while (suffixIdx >= 0) {
        val value = suffix(suffixIdx)
        bytes(idx) = (value >> 8).toByte
        bytes(idx + 1) = value.toByte
        suffixIdx -= 1
        idx += 2
      }
      Some(unsafeFromBytes(bytes))
    }
  }

  private val MixedStringFormat = """([:a-fA-F0-9]+:)(\d+\.\d+\.\d+\.\d+)""".r
  private def fromMixedString(value: String): Option[Ipv6Address] =
    value match {
      case MixedStringFormat(prefix, v4Str) =>
        for {
          pfx <- fromNonMixedString(prefix + "0:0")
          v4 <- Ipv4Address.fromString(v4Str)
        } yield {
          val bytes = pfx.toBytes
          val v4bytes = v4.toBytes
          bytes(12) = v4bytes(0)
          bytes(13) = v4bytes(1)
          bytes(14) = v4bytes(2)
          bytes(15) = v4bytes(3)
          unsafeFromBytes(bytes)
        }
      case _ => None
    }

  /** Constructs an IPv6 address from a 16-element byte array. Returns `Some` when array is exactly 16-bytes and `None`
    * otherwise.
    */
  def fromBytes(bytes: Array[Byte]): Option[Ipv6Address] =
    if (bytes.size == 16) Some(unsafeFromBytes(bytes.clone))
    else None

  private def unsafeFromBytes(bytes: Array[Byte]): Ipv6Address =
    new Ipv6Address(bytes)

  /** Constructs an address from the specified 16 bytes.
    *
    * Each byte is represented as an `Int` to avoid having to manually call `.toByte` on each value -- the `toByte` call
    * is done inside this function.
    */
  def fromBytes(
      b0: Int,
      b1: Int,
      b2: Int,
      b3: Int,
      b4: Int,
      b5: Int,
      b6: Int,
      b7: Int,
      b8: Int,
      b9: Int,
      b10: Int,
      b11: Int,
      b12: Int,
      b13: Int,
      b14: Int,
      b15: Int
  ): Ipv6Address = {
    val bytes = new Array[Byte](16)
    bytes(0) = b0.toByte
    bytes(1) = b1.toByte
    bytes(2) = b2.toByte
    bytes(3) = b3.toByte
    bytes(4) = b4.toByte
    bytes(5) = b5.toByte
    bytes(6) = b6.toByte
    bytes(7) = b7.toByte
    bytes(8) = b8.toByte
    bytes(9) = b9.toByte
    bytes(10) = b10.toByte
    bytes(11) = b11.toByte
    bytes(12) = b12.toByte
    bytes(13) = b13.toByte
    bytes(14) = b14.toByte
    bytes(15) = b15.toByte
    unsafeFromBytes(bytes)
  }

  /** Constructs an IPv6 address from a `BigInt`, using the lower 128-bits. */
  def fromBigInt(value: BigInt): Ipv6Address = {
    val bytes = new Array[Byte](16)
    var rem = value
    for (i <- 15 to 0 by -1) {
      bytes(i) = (rem & 0x0ff).toByte
      rem = rem >> 8
    }
    unsafeFromBytes(bytes)
  }

  /** Computes a mask by setting the first / left-most `n` bits high.
    *
    * @example {{{
    * scala> Ipv6Address.mask(32)
    * res0: Ipv6Address = ffff:ffff::
    * }}}
    */
  def mask(bits: Int): Ipv6Address = {
    val b = if (bits < 0) 0 else if (bits > 128) 128 else bits
    Ipv6Address
      .fromBigInt {
        if (b == 128) (BigInt(-1L) << 64) | BigInt(-1L)
        else if (b < 64) BigInt(~(-1L >>> b)) << 64
        else (BigInt(-1L) << 64) | BigInt(~(-1L >>> (b - 64)))
      }
  }
}

/** Internationalized domain name, as specified by RFC3490 and RFC5891.
  *
  * This type models internationalized hostnames. An IDN provides unicode labels, a unicode string form, and an ASCII
  * hostname form.
  *
  * A well formed IDN consists of one or more labels separated by dots. Each label may contain unicode characters as
  * long as the converted ASCII form meets the requirements of RFC1123 (e.g., 63 or fewer characters and no leading or
  * trailing dash). A dot is represented as an ASCII period or one of the unicode dots: full stop, ideographic full
  * stop, fullwidth full stop, halfwidth ideographic full stop.
  *
  * The `toString` method returns the IDN in the form in which it was constructed. Sometimes it is useful to normalize
  * the IDN -- converting all dots to an ASCII period and converting all labels to lowercase.
  *
  * Note: equality and comparison of IDNs is case-sensitive. Consider comparing normalized toString values for a more
  * lenient notion of equality.
  */
final class IDN private (val labels: List[IDN.Label], val hostname: Hostname, override val toString: String)
    extends Host {

  /** Converts this IDN to lower case and replaces dots with ASCII periods. */
  def normalized: IDN = {
    val newLabels = labels.map(l => new IDN.Label(l.toString.toLowerCase))
    new IDN(newLabels, hostname.normalized, newLabels.toList.mkString("."))
  }

  override def hashCode: Int = MurmurHash3.stringHash(toString, "IDN".hashCode)
  override def equals(other: Any): Boolean =
    other match {
      case that: IDN => toString == that.toString
      case _         => false
    }
}

object IDN extends IDNCompanionPlatform {

  /** Label component of an IDN. */
  final class Label private[IDN] (override val toString: String) extends Serializable with Ordered[Label] {
    def compare(that: Label): Int = toString.compare(that.toString)
    override def hashCode: Int = MurmurHash3.stringHash(toString, "Label".hashCode)
    override def equals(other: Any): Boolean =
      other match {
        case that: Label => toString == that.toString
        case _           => false
      }
  }

  private val DotPattern = "[\\.\u002e\u3002\uff0e\uff61]"

  /** Constructs a `IDN` from a string. */
  def fromString(value: String): Option[IDN] =
    value.size match {
      case 0 => None
      case _ =>
        val labels = value
          .split(DotPattern)
          .iterator
          .map(new Label(_))
          .toList
        Option(labels).filterNot(_.isEmpty).flatMap { ls =>
          val hostname = toAscii(value).flatMap(Hostname.fromString)
          hostname.map(h => new IDN(ls, h, value))
        }
    }

  /** Converts the supplied (ASCII) hostname in to an IDN. */
  def fromHostname(hostname: Hostname): IDN = {
    val labels =
      hostname.labels.map(l => new Label(toUnicode(l.toString)))
    new IDN(labels, hostname, labels.toList.mkString("."))
  }

  implicit val show: Show[IDN] = Show.fromToString[IDN]
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy