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

io.lemonlabs.uri.Host.scala Maven / Gradle / Ivy

The newest version!
package io.lemonlabs.uri
import cats.{Eq, Order, Show}
import io.lemonlabs.uri.config.UriConfig
import io.lemonlabs.uri.inet._
import io.lemonlabs.uri.parsing.UrlParser

import scala.annotation.tailrec
import scala.collection.immutable
import scala.util.Try

sealed trait Host {
  type Self <: Host
  def conf: UriConfig
  def value: String
  override def toString: String = value

  /** Copies this Host but with a new UriConfig
    *
    * @param config the new config to use
    * @return a new Host with the specified config
    */
  def withConfig(config: UriConfig): Self

  /** Returns the longest public suffix for the host in this URI. Examples include:
    *  `com`   for `www.example.com`
    *  `co.uk` for `www.example.co.uk`
    *
    * @return the longest public suffix for the host in this URI
    */
  def publicSuffix: Option[String]

  /** Returns all longest public suffixes for the host in this URI. Examples include:
    *  `com` for `www.example.com`
    *  `co.uk` and `uk` for `www.example.co.uk`
    *
    * @return all public suffixes for the host in this URI
    */
  def publicSuffixes: Vector[String]

  /** @return the domain name in ASCII Compatible Encoding (ACE), as defined by the ToASCII
    *         operation of RFC 3490.
    */
  def toStringPunycode: String = value

  /** Returns the apex domain for this Host.
    *
    * The apex domain is constructed from the public suffix prepended with the immediately preceding
    * dot segment.
    *
    * Examples include:
    *  `example.com`   for `www.example.com`
    *  `example.co.uk` for `www.example.co.uk`
    *
    * @return the apex domain for this domain
    */
  def apexDomain: Option[String]

  /** Returns the second largest subdomain for this URL's host.
    *
    * E.g. for http://a.b.c.example.com returns a.b.c
    *
    * Note: In the event there is only one subdomain (i.e. the host is the apex domain), this method returns `None`.
    * E.g. This method will return `None` for `http://example.com`.
    *
    * @return the second largest subdomain for this URL's host
    */
  def subdomain: Option[String]

  /** Returns all subdomains for this URL's host.
    * E.g. for http://a.b.c.example.com returns a, a.b, a.b.c and a.b.c.example
    *
    * @return all subdomains for this URL's host
    */
  def subdomains: Vector[String]

  /** Returns the shortest subdomain for this URL's host.
    * E.g. for http://a.b.c.example.com returns a
    *
    * @return the shortest subdomain for this URL's host
    */
  def shortestSubdomain: Option[String]

  /** Returns the longest subdomain for this URL's host.
    * E.g. for http://a.b.c.example.com returns a.b.c.example
    *
    * @return the longest subdomain for this URL's host
    */
  def longestSubdomain: Option[String]

  /** Returns this host with its character case normalized according to
    * RFC 3986
    */
  def normalize: Self
}

object Host {
  def parseTry(s: CharSequence)(implicit config: UriConfig = UriConfig.default): Try[Host] =
    UrlParser.parseHost(s.toString)

  def parseOption(s: CharSequence)(implicit config: UriConfig = UriConfig.default): Option[Host] =
    parseTry(s).toOption

  def parse(s: CharSequence)(implicit config: UriConfig = UriConfig.default): Host =
    parseTry(s).get

  def unapply(host: Host): Option[String] =
    Some(host.toString)

  implicit val eqHost: Eq[Host] = Eq.fromUniversalEquals
  implicit val showHost: Show[Host] = Show.fromToString
  implicit val orderHost: Order[Host] = Order.by(_.value)
}

final case class DomainName(value: String)(implicit val conf: UriConfig = UriConfig.default)
    extends Host
    with PunycodeSupport {
  type Self = DomainName

  private def isValidPublicSuffix(suffix: String): Boolean =
    if (PublicSuffixes.set.contains(suffix)) { true }
    else if (PublicSuffixes.exceptions.contains(suffix)) { false }
    else {
      val dotIndex = suffix.indexOf('.')
      if (dotIndex < 1) { false }
      else {
        PublicSuffixes.wildcardPrefixes.contains(suffix.substring(dotIndex + 1))
      }
    }

  def withConfig(config: UriConfig): DomainName =
    DomainName(value)(config)

  /** Returns the longest public suffix for the host in this URI. Examples include:
    *  `com`   for `www.example.com`
    *  `co.uk` for `www.example.co.uk`
    *
    * @return the longest public suffix for the host in this URI
    */
  def publicSuffix: Option[String] = {
    @scala.annotation.tailrec
    def findLongest(remaining: String): Option[String] = {
      if (isValidPublicSuffix(remaining)) {
        Some(remaining)
      } else {
        val i = remaining.indexOf('.')
        if (i == -1)
          None
        else
          findLongest(remaining.substring(i + 1))
      }
    }
    findLongest(value)
  }

  /** Returns all public suffixes for the host in this URI. Examples include:
    *  `com` for `www.example.com`
    *  `co.uk` and `uk` for `www.example.co.uk`
    *
    * @return all public suffixes for the host in this URI
    */
  def publicSuffixes: Vector[String] = {
    @scala.annotation.tailrec
    def findAll(remaining: String, matches: Vector[String]): Vector[String] = {
      val newMatches =
        if (isValidPublicSuffix(remaining)) matches :+ remaining
        else matches
      val i = remaining.indexOf('.')
      if (i == -1)
        newMatches
      else
        findAll(remaining.substring(i + 1), newMatches)
    }
    findAll(value, Vector.empty)
  }

  /** @return the domain name in ASCII Compatible Encoding (ACE), as defined by the ToASCII
    *         operation of RFC 3490.
    */
  override def toStringPunycode: String =
    toPunycode(value)

  /** Returns the apex domain for this Host.
    *
    * The apex domain is constructed from the public suffix prepended with the immediately preceding
    * dot segment.
    *
    * Examples include:
    *  `example.com`   for `www.example.com`
    *  `example.co.uk` for `www.example.co.uk`
    *
    * @return the apex domain for this domain
    */
  def apexDomain: Option[String] =
    publicSuffix map { ps =>
      val apexDomainStart = value.dropRight(ps.length + 1).lastIndexOf('.')

      if (apexDomainStart == -1) value
      else value.substring(apexDomainStart + 1)
    }

  /** Returns the second largest subdomain in this host.
    *
    * E.g. for http://a.b.c.example.com returns a.b.c
    *
    * Note: In the event there is only one subdomain (i.e. the host is the apex domain), this method returns `None`.
    * E.g. This method will return `None` for `http://example.com`.
    *
    * @return the second largest subdomain for this host
    */
  def subdomain: Option[String] =
    longestSubdomain flatMap { ls =>
      ls.lastIndexOf('.') match {
        case -1 => None
        case i  => Some(ls.substring(0, i))
      }
    }

  /** Returns all subdomains for this host.
    * E.g. for http://a.b.c.example.com returns a, a.b, a.b.c and a.b.c.example
    *
    * @return all subdomains for this host
    */
  def subdomains: Vector[String] = {
    def concatHostParts(longestSubdomainStr: String) = {
      val parts = longestSubdomainStr.split('.').toVector
      if (parts.size == 1) parts
      else {
        parts.tail.foldLeft(Vector(parts.head)) { (subdomainList, part) =>
          subdomainList :+ subdomainList.last + '.' + part
        }
      }
    }

    longestSubdomain.map(concatHostParts).getOrElse(Vector.empty)
  }

  /** Returns the shortest subdomain for this host.
    * E.g. for http://a.b.c.example.com returns a
    *
    * @return the shortest subdomain for this host
    */
  def shortestSubdomain: Option[String] =
    longestSubdomain.map(_.takeWhile(_ != '.'))

  /** Returns the longest subdomain for this host.
    * E.g. for http://a.b.c.example.com returns a.b.c.example
    *
    * @return the longest subdomain for this host
    */
  def longestSubdomain: Option[String] = {
    val publicSuffixLength: Int = publicSuffix.map(_.length + 1).getOrElse(0)
    value.dropRight(publicSuffixLength) match {
      case ""    => None
      case other => Some(other)
    }
  }

  /** Returns this host normalized according to
    * RFC 3986
    */
  def normalize: Self = this.copy(value.toLowerCase)
}

object DomainName {
  def parseTry(s: CharSequence)(implicit config: UriConfig = UriConfig.default): Try[DomainName] =
    UrlParser.parseDomainName(s.toString)

  def parseOption(s: CharSequence)(implicit config: UriConfig = UriConfig.default): Option[DomainName] =
    parseTry(s).toOption

  def parse(s: CharSequence)(implicit config: UriConfig = UriConfig.default): DomainName =
    parseTry(s).get

  def empty: DomainName = DomainName("")

  implicit val eqDomainName: Eq[DomainName] = Eq.fromUniversalEquals
  implicit val showDomainName: Show[DomainName] = Show.fromToString
  implicit val orderDomainName: Order[DomainName] = Order.by(_.value)
}

final case class IpV4(octet1: Byte, octet2: Byte, octet3: Byte, octet4: Byte)(implicit
    val conf: UriConfig = UriConfig.default
) extends Host {
  type Self = IpV4
  private def uByteToInt(b: Byte): Int = b & 0xff

  def octet1Int: Int = uByteToInt(octet1)
  def octet2Int: Int = uByteToInt(octet2)
  def octet3Int: Int = uByteToInt(octet3)
  def octet4Int: Int = uByteToInt(octet4)

  def octets: Vector[Byte] = Vector(octet1, octet2, octet3, octet4)
  def octetsInt: Vector[Int] = Vector(octet1Int, octet2Int, octet3Int, octet4Int)

  def toIpV6Pieces: (Char, Char) = {
    val piece1 = (octet1Int << 8) + octet2Int
    val piece2 = (octet3Int << 8) + octet4Int
    (piece1.toChar, piece2.toChar)
  }

  def withConfig(config: UriConfig): IpV4 =
    IpV4(octet1, octet2, octet3, octet4)(config)

  def value: String = s"$octet1Int.$octet2Int.$octet3Int.$octet4Int"

  def apexDomain: Option[String] = None
  def publicSuffix: Option[String] = None
  def publicSuffixes: Vector[String] = Vector.empty
  def subdomain: Option[String] = None
  def subdomains: Vector[String] = Vector.empty
  def shortestSubdomain: Option[String] = None
  def longestSubdomain: Option[String] = None

  def normalize: Self = this
}

object IpV4 {
  def apply(octet1: Int, octet2: Int, octet3: Int, octet4: Int): IpV4 = {
    require(octet1 >= 0 && octet2 >= 0 && octet3 >= 0 && octet4 >= 0, "Octets must be >= 0")
    require(octet1 <= 255 && octet2 <= 255 && octet3 <= 255 && octet4 <= 255, "Octets must be <= 255")

    new IpV4(octet1.toByte, octet2.toByte, octet3.toByte, octet4.toByte)
  }

  def parseTry(s: CharSequence)(implicit config: UriConfig = UriConfig.default): Try[IpV4] =
    UrlParser.parseIpV4(s.toString)

  def parseOption(s: CharSequence)(implicit config: UriConfig = UriConfig.default): Option[IpV4] =
    parseTry(s).toOption

  def parse(s: CharSequence)(implicit config: UriConfig = UriConfig.default): IpV4 =
    parseTry(s).get

  def localhost: IpV4 = IpV4(127, 0, 0, 1)

  implicit val eqIpV4: Eq[IpV4] = Eq.fromUniversalEquals
  implicit val showIpV4: Show[IpV4] = Show.fromToString
  implicit val orderIpV4: Order[IpV4] = Order.by(_.octets)
}

final case class IpV6(piece1: Char,
                      piece2: Char,
                      piece3: Char,
                      piece4: Char,
                      piece5: Char,
                      piece6: Char,
                      piece7: Char,
                      piece8: Char
)(implicit val conf: UriConfig = UriConfig.default)
    extends Host {
  type Self = IpV6

  def piece1Int: Int = piece1.toInt
  def piece2Int: Int = piece2.toInt
  def piece3Int: Int = piece3.toInt
  def piece4Int: Int = piece4.toInt
  def piece5Int: Int = piece5.toInt
  def piece6Int: Int = piece6.toInt
  def piece7Int: Int = piece7.toInt
  def piece8Int: Int = piece8.toInt

  val pieces: Vector[Char] = Vector(piece1, piece2, piece3, piece4, piece5, piece6, piece7, piece8)
  def hexPieces: Vector[String] = pieces.map(hex)

  private def hex(c: Char): String = Integer.toHexString(c.toInt)

  /** Finds the longest run of two or more zeros in this IPv6
    * Returns the start and end index of the run
    * Returns (-1, -1) if there is no run
    */
  private def elidedStartAndEnd(): (Int, Int) = {
    @tailrec def longestRun(index: Int, longest: (Int, Int), currentRunStart: Int): (Int, Int) = {
      def newLongest = {
        val newLength = index - currentRunStart
        if (newLength > 1 && newLength > longest._2 - longest._1) (currentRunStart, index) else longest
      }

      if (index == 8)
        newLongest
      else if (pieces(index) != 0)
        longestRun(index + 1, newLongest, index + 1)
      else
        longestRun(index + 1, longest, currentRunStart)
    }

    longestRun(0, (-1, -1), 0)
  }

  def withConfig(config: UriConfig): IpV6 =
    IpV6(piece1, piece2, piece3, piece4, piece5, piece6, piece7, piece8)(config)

  def apexDomain: Option[String] = None
  def publicSuffix: Option[String] = None
  def publicSuffixes: Vector[String] = Vector.empty
  def subdomain: Option[String] = None
  def subdomains: Vector[String] = Vector.empty
  def shortestSubdomain: Option[String] = None
  def longestSubdomain: Option[String] = None

  def value: String =
    elidedStartAndEnd() match {
      case (-1, -1) => toStringNonNormalised
      case (start, end) =>
        "[" + hexPieces.take(start).mkString(":") + "::" + hexPieces.drop(end).mkString(":") + "]"
    }

  def toStringNonNormalised: String = hexPieces.mkString("[", ":", "]")

  def normalize: IpV6 = this
}

object IpV6 {
  def apply(piece1: Int,
            piece2: Int,
            piece3: Int,
            piece4: Int,
            piece5: Int,
            piece6: Int,
            piece7: Int,
            piece8: Int
  ): IpV6 = {
    require(
      piece1 >= 0 && piece2 >= 0 && piece3 >= 0 && piece4 >= 0 &&
        piece5 >= 0 && piece6 >= 0 && piece7 >= 0 && piece8 >= 0,
      "IPv6 pieces must be >= 0"
    )
    require(
      piece1 <= Char.MaxValue && piece2 <= Char.MaxValue && piece3 <= Char.MaxValue && piece4 <= Char.MaxValue &&
        piece5 <= Char.MaxValue && piece6 <= Char.MaxValue && piece7 <= Char.MaxValue && piece8 <= Char.MaxValue,
      "IPv6 Pieces must be <= " + Char.MaxValue.toInt
    )
    new IpV6(
      piece1.toChar,
      piece2.toChar,
      piece3.toChar,
      piece4.toChar,
      piece5.toChar,
      piece6.toChar,
      piece7.toChar,
      piece8.toChar
    )
  }

  private def hexToInt(hex: String) = Integer.parseInt(hex, 16)

  def apply(piece1: String,
            piece2: String,
            piece3: String,
            piece4: String,
            piece5: String,
            piece6: String,
            piece7: String,
            piece8: String
  ): IpV6 = {
    IpV6(
      hexToInt(piece1),
      hexToInt(piece2),
      hexToInt(piece3),
      hexToInt(piece4),
      hexToInt(piece5),
      hexToInt(piece6),
      hexToInt(piece7),
      hexToInt(piece8)
    )
  }

  def apply(piece1: String,
            piece2: String,
            piece3: String,
            piece4: String,
            piece5: String,
            piece6: String,
            piece78: IpV4
  ): IpV6 = {
    val (piece7, piece8) = piece78.toIpV6Pieces
    IpV6(
      hexToInt(piece1),
      hexToInt(piece2),
      hexToInt(piece3),
      hexToInt(piece4),
      hexToInt(piece5),
      hexToInt(piece6),
      piece7,
      piece8
    )
  }

  def fromIntPieces(pieces: immutable.Seq[Int]): IpV6 = {
    require(pieces.length == 8, "IPv6 must be made up of eight pieces")
    IpV6(pieces(0), pieces(1), pieces(2), pieces(3), pieces(4), pieces(5), pieces(6), pieces(7))
  }

  def fromHexPieces(pieces: immutable.Seq[String]): IpV6 = {
    require(pieces.length == 8, "IPv6 must be made up of eight pieces")
    IpV6(pieces(0), pieces(1), pieces(2), pieces(3), pieces(4), pieces(5), pieces(6), pieces(7))
  }

  def fromHexPiecesAndIpV4(pieces: immutable.Seq[String], ls32: IpV4): IpV6 = {
    require(pieces.length == 6, "IPv6 must be made up of six pieces when least-significant 32bits are an IPv4")
    IpV6(pieces(0), pieces(1), pieces(2), pieces(3), pieces(4), pieces(5), ls32)
  }

  def parseTry(s: CharSequence)(implicit config: UriConfig = UriConfig.default): Try[IpV6] =
    UrlParser.parseIpV6(s.toString)

  def parseOption(s: CharSequence)(implicit config: UriConfig = UriConfig.default): Option[IpV6] =
    parseTry(s).toOption

  def parse(s: CharSequence)(implicit config: UriConfig = UriConfig.default): IpV6 =
    parseTry(s).get

  def localhost: IpV6 = IpV6(0, 0, 0, 0, 0, 0, 0, 1)

  implicit val eqIpV6: Eq[IpV6] = Eq.fromUniversalEquals
  implicit val showIpV6: Show[IpV6] = Show.fromToString
  implicit val orderIpV6: Order[IpV6] = Order.by(_.pieces)
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy