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

spinoco.protocol.http.codec.helper.scala Maven / Gradle / Ivy

package spinoco.protocol.http.codec

import java.nio.charset.StandardCharsets
import java.time.{LocalDateTime, ZoneId, ZonedDateTime}
import java.time.format.DateTimeFormatter

import scodec.{Attempt, Codec, DecodeResult, Err, SizeBound}
import scodec.bits.{BitVector, ByteVector}
import scodec.codecs._
import spinoco.protocol.common.codec._
import spinoco.protocol.common.util.attempt

import scala.annotation.tailrec

/**
  * Created by pach on 13/01/17.
  */
object helper {

  val comma = ByteVector(',')
  val SP = ByteVector(' ')
  val comma_SP = ByteVector(',',' ')

  val colon = ByteVector(':')
  val semicolon = ByteVector(';')
  val semicolon_SP = ByteVector(';',' ')
  val dash = ByteVector('-')

  val slash = ByteVector('/')
  val star = ByteVector('*')
  val qPar = ByteVector('q','=')

  val amp = ByteVector('&')

  val _equal = ByteVector('=')

  val `://` = ByteVector(':','/','/')

  val crlf = ByteVector('\r','\n')

  val trimmedAsciiToken = ignoreWS ~> asciiToken <~ ignoreWS

  /**
    * Codec for decoding comma delimited parameters from header.
    * When encoding, the comma is always followed by [SP]
    *
    * When decoding, the parameters are tested if they are quoted strings and if yes,
    * the comma is skipped if within quotes
    *
    */
  def commaDelimited[A](valueCodec: Codec[A]): Codec[List[A]] =
    delimitedBy(comma, comma_SP, valueCodec)

  def commaDelimitedMin[A](codec:Codec[A], min:Int):Codec[List[A]] = {
    def guard(items: List[A]):Attempt[List[A]] = {
      if (items.size < min) Attempt.failure(Err(s"Required $min parameters, but got $items"))
      else Attempt.successful(items)
    }
    commaDelimited(codec).exmap(guard,guard)
  }

  /**
    * Codec for decoding delimited parameters from header.
    * When encoding, the encodeBy is used to encode `A` delimiter
    *
    * When decoding, the parameters are tested if they are quoted strings and if yes,
    * the `by` is skipped if within quotes
    *
    */
  def delimitedBy[A](by: ByteVector, encodeBy: ByteVector, valueCodec: Codec[A]): Codec[List[A]] = {
    listMultiplexed(
      _ ++ encodeBy.bits ++ _
      , bits => splitByQuoted(by, bits)
      , valueCodec)
  }

  /** codec for qValue in multiple parameter lists**/
  val qValueCodec: Codec[Float] = {
    constant(qPar) ~> floatAsString
  }

  /** splits supplied content by supplied delimiter. Matches delimiter by each byte **/
  private def splitBy(delimiter: ByteVector, content: BitVector):(BitVector, BitVector) = {
    val bs = content.bytes
    bs.indexOfSlice(delimiter) match {
      case -1 => content -> BitVector.empty
      case at =>
        val (h,t) = bs.splitAt(at)
        h.bits -> t.drop(delimiter.size).bits
    }
  }

  /** splits by delimiter, honoring quoted commas **/
  def splitByQuoted(delimiter: ByteVector, content:BitVector):(BitVector, BitVector) = {
    val bs = content.bytes
    @tailrec
    def go(rem: ByteVector, inQuote: Boolean):(BitVector, BitVector) = {
      if (rem.isEmpty) (content, BitVector.empty)
      else if (rem.head == '"') {
        if (inQuote) go(rem.tail, inQuote = false)
        else go(rem.tail, inQuote = true)
      } else {
        if (inQuote) go(rem.tail, inQuote = true)
        else {
          if (! rem.startsWith(delimiter)) go(rem.tail, inQuote = false)
          else {
            val t = rem.drop(delimiter.length)
            val h = bs.take(bs.length - t.length - delimiter.length)
            h.bits -> t.bits
          }
        }
      }
    }
    go (bs, inQuote = false)

  }


  def splitByWS(content: BitVector): (BitVector, BitVector) = {
    val hws = content.bytes.dropWhile(_.toChar.isWhitespace)
    val h = hws.takeWhile {! _.toChar.isWhitespace }
    val t = hws.drop(h.size).dropWhile(_.toChar.isWhitespace)
    (h.bits, t.bits)
  }



  /**
    * A codec, that allows to encode/decode parametrized values `A` by parameter `B`.
    * Parameter is optional and is discriminated by `discriminator`
    */
  def parametrized[A,B](discriminator:ByteVector, aCodec:Codec[A], bCodec:Codec[B]):Codec[(A,Option[B])] =
    parametrized2(discriminator,discriminator,aCodec, bCodec)




  def parametrized2[A,B](discriminator:ByteVector, encodingDiscriminator: ByteVector, aCodec:Codec[A], bCodec:Codec[B]):Codec[(A,Option[B])] = {
    parametrizedN(discriminator, encodingDiscriminator, aCodec, bCodec).exmap(
      { case (a, bl) =>
        if (bl.size <= 1) Attempt.successful(a -> bl.headOption)
        else Attempt.failure(Err(s"Only one parameter allowed, got $bl"))
      }
      , { case (a, bo) => Attempt.successful(a -> bo.toList) }
    )
  }

  /**
    * A codec, that allows to encode/decode parametrized values `A` by optional parameters of `B`.
    * Parameter is optional (may be N), and is discriminated by `discriminator`
    *
    * furthermore if the discriminator is found in quotes (") it is ignored until cloiong quotes are found
    */
  def parametrizedN[A,B](discriminator:ByteVector, encodingDiscriminator: ByteVector, aCodec:Codec[A], bCodec:Codec[B]):Codec[(A,List[B])] = {
    def encode(in:(A, List[B])):Attempt[List[BitVector]] = {
      in match {
        case (a, bl) => aCodec.encode(a).flatMap { av =>
          @tailrec
          def go(acc: List[BitVector], rem: List[B]):Attempt[List[BitVector]] = {
            rem.headOption match {
              case None => Attempt.successful(acc)
              case Some(b) => bCodec.encode(b) match {
                case Attempt.Successful(bv) => go(acc :+ bv, rem.tail)
                case failure: Attempt.Failure => failure
              }
            }
          }
          go(List.empty, bl).map { av +: _ }
        }
      }
    }

    def decode(segments: List[BitVector]):Attempt[(A, List[B])] = {
      segments match {
        case List() => Attempt.failure(Err(s"Expected value with optional parameter, got none"))
        case a :: tail =>
          aCodec.decodeValue(a).flatMap { av =>
            @tailrec
            def go(acc:List[B], rem: List[BitVector]):Attempt[List[B]] = {
              rem.headOption match {
                case None => Attempt.successful(acc)
                case Some(b) => bCodec.decodeValue(b) match {
                  case Attempt.Successful(bv) => go(acc :+ bv, rem.tail)
                  case failure: Attempt.Failure => failure
                }
              }
            }
            go(List.empty, tail).map { av -> _ }
          }
      }
    }


    listMultiplexed(
      _ ++ encodingDiscriminator.bits ++ _
      , bits => splitByQuoted(discriminator, bits)
      , bitsWsRemoved
    ).exmap(decode,encode)
  }




  /** string encoded // decoded as ASCII **/
  val asciiString: Codec[String] = string(StandardCharsets.US_ASCII)

  /** string encoded // decoded as UTF8 **/
  val utf8String: Codec[String] = string(StandardCharsets.UTF_8)



  /** utf8 string that is trimmed of WS **/
  val trimmedUtf8String: Codec[String] = utf8String.xmap(_.trim, _.trim)

  private val quotedChars: Set[Char] = "()<>@.,;:\\/[]?={} \t".toSet



  val alwaysQuotedUtf8String: Codec[String] = {
    utf8String.exmap(
      s => {
        val st = s.trim
        if (st.startsWith("\"") && s.endsWith("\"")) Attempt.successful(st.drop(1).dropRight(1))
        else Attempt.failure(Err(s"String must be quoted: $s"))
      }
      , s => Attempt.successful( "\"" + s + "\"" )
    )
  }

  /**
    * Encodes as supplied ascii string constant. Drops any leading whitespace if any.
    * @param const        Constant to encode // decode
    * @param ignoreCase   When true, the decoding ignores case
    * @return
    */
  def asciiConstant(const: String, ignoreCase: Boolean = true): Codec[Unit] = {
    val example = ByteVector.view(const.getBytes)

    new Codec[Unit] {
      def decode(bits: BitVector): Attempt[DecodeResult[Unit]] = {
        val present = bits.bytes.dropWhile(_.toChar.isWhitespace)

        if (present.take(example.size) == example) Attempt.successful(DecodeResult((), present.drop(example.size).bits))
        else if (! ignoreCase || present.size != example.size) Attempt.failure(Err(s"Expected $const, got ${bits.take(example.size).decodeUtf8}"))
        else {
          present.take(example.size).decodeAscii match {
            case Left(ex) => Attempt.failure(Err(s"Invalid coding of input string: ${ex.getMessage}"))
            case Right(s) =>
              if (const.equalsIgnoreCase(s)) Attempt.successful(DecodeResult((), present.drop(example.size).bits))
              else Attempt.failure(Err(s"Expected $const, got ${bits.take(example.size).decodeUtf8}"))
          }

        }

      }
      def encode(value: Unit): Attempt[BitVector] = Attempt.successful(example.bits)
      def sizeBound: SizeBound = SizeBound.exact(example.size)
    }
  }

  /** codec that decodes until whitespace character is found. Encodes as ascii string **/
  def asciiStringNoWs: Codec[String] =
    asciiStringUntil(! _.isWhitespace)

  def asciiStringUntil(f: Char => Boolean): Codec[String] = {
    bytesUntil(b => f(b.toChar)).exmap (
      bs => asciiString.decodeValue(bs.bits)
      , s => asciiString.encode(s).map(_.bytes)
    )
  }

  /** codec that strips all whitespace, and encodes as supplied string. At least one whitespace is mandatory. **/
  def whitespace(encodeAs: String = " "): Codec[Unit] = {
    val content = ByteVector.view(encodeAs.getBytes)
    new Codec[Unit] {
      def sizeBound: SizeBound = SizeBound.unknown
      def encode(value: Unit): Attempt[BitVector] = Attempt.successful(content.bits)
      def decode(bits: BitVector): Attempt[DecodeResult[Unit]] = {
        val h = bits.bytes.takeWhile(_.toChar.isWhitespace)
        if (h.size == 0) Attempt.failure(Err(s"Expected whitespace, got ${bits.decodeUtf8}"))
        else {
          val t = bits.drop(h.size*8)
          Attempt.successful(DecodeResult((), t))
        }
      }
    }
  }

  /** codec that succeeds, iff star (*) is present **/
  val starCodec: Codec[Unit] = trimmedAsciiToken.exmap(
    s => if (s == "*") Attempt.successful(()) else Attempt.failure(Err("Expected *"))
    , _ => Attempt.successful("*")
  )


  private val dateFormatRFC1123 = DateTimeFormatter.RFC_1123_DATE_TIME

  /** encodes // decodes time by http time format **/
  val httpDateTimeCodec: Codec[LocalDateTime] = {
    def decode(s:String):Attempt[LocalDateTime] = {
      // todo: support for non rfc 1213 formats
       attempt { ZonedDateTime.from(dateFormatRFC1123.parse(s.replace('-', ' '))).toLocalDateTime }
    }

    (ignoreWS ~> asciiString).exmap(
      decode
      , ldt => Attempt.successful(localDateTime2String(ldt))
    )
  }

  private val GMTZone = ZoneId.of("GMT")

  /** RFC 1123 date 2 string **/
  def localDateTime2String(ldt: LocalDateTime):String = {
    //ldt.toString
    dateFormatRFC1123.format(ldt.atZone(GMTZone))
  }


  val base64Encoded: Codec[ByteVector] = {
    utf8String.exmap(
      s => ByteVector.fromBase64(s.trim).map(Attempt.successful).getOrElse(Attempt.failure(Err(s"Invalid BASE64 encoding: $s")))
      , bv => Attempt.successful(bv.toBase64)
    )
  }

  /** codec that decodes to Some(a) when codec decodes or None, when there are no nonempty characters left **/
  def orEmpty[A](codec:Codec[A]):Codec[Option[A]] = new Codec[Option[A]] {
    def decode(bits: BitVector): Attempt[DecodeResult[Option[A]]] = {
      codec.decode(bits).map(_.map(Some(_))) recoverWith {
        case err =>
          if (bits.bytes.dropWhile(_.toChar.isWhitespace).isEmpty) Attempt.successful(DecodeResult(None, BitVector.empty))
          else Attempt.failure(err)
      }
    }

    def encode(value: Option[A]): Attempt[BitVector] = {
      value match {
        case None => Attempt.successful(BitVector.empty)
        case Some(a) => codec.encode(a)
      }
    }

    def sizeBound: SizeBound = codec.sizeBound.atMost
  }

  /** decodes as utf8 string, that will return string until pattern is matched **/
  def utf8StringUntil(until: String):Codec[String] = new Codec[String] {
    // todo byke this more effective by decoding string as we match the sample
    def decode(bits: BitVector): Attempt[DecodeResult[String]] = {
      utf8.decode(bits).flatMap { case dr@DecodeResult(s,rem) =>
       s.indexOf(until) match {
         case idx if idx < 0 => Attempt.successful(dr)
         case idx =>
           val (h, t) = s.splitAt(idx)
           Attempt.successful(DecodeResult(h, BitVector.view(t.getBytes) ++ rem))
       }
      }
    }

    def encode(value: String): Attempt[BitVector] =
      utf8.encode(value)

    def sizeBound: SizeBound = SizeBound.unknown
  }



}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy