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

zio.http.internal.FormAST.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2021 - 2023 Sporta Technologies PVT LTD & the ZIO HTTP contributors.
 *
 * 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 zio.http.internal

import java.nio.charset._

import zio._

import zio.http.Header.ContentTransferEncoding
import zio.http.{Boundary, Headers, MediaType}

private[http] sealed trait FormAST {
  def bytes: Chunk[Byte]

  def isContent: Boolean = this match {
    case FormAST.Content(_) => true
    case _                  => false
  }
}

private[http] object FormAST {

  sealed trait DecodingPart1AST extends FormAST

  sealed trait DecodingPart2AST extends DecodingPart1AST

  def makePart1(
    bytes: Chunk[Byte],
    boundary: Boundary,
    encoding: Charset = StandardCharsets.UTF_8,
  ): DecodingPart1AST = {
    val header = Header.fromBytes(bytes.toArray, encoding)
    header match {
      case Some(header)                            => header
      case None if boundary.isEncapsulating(bytes) => EncapsulatingBoundary(boundary)
      case None if boundary.isClosing(bytes)       => ClosingBoundary(boundary)
      case None                                    => Content(bytes)
    }
  }

  def makePart2(
    bytes: Chunk[Byte],
    boundary: Boundary,
  ): DecodingPart2AST = {
    if (boundary.isEncapsulating(bytes)) EncapsulatingBoundary(boundary)
    else if (boundary.isClosing(bytes)) ClosingBoundary(boundary)
    else Content(bytes)
  }

  case object EoL extends FormAST { val bytes: Chunk[Byte] = Chunk('\r', '\n') }

  final case class EncapsulatingBoundary(boundary: Boundary) extends DecodingPart2AST {
    def bytes: Chunk[Byte] = boundary.encapsulationBoundaryBytes
  }

  final case class ClosingBoundary(boundary: Boundary) extends DecodingPart2AST {
    def bytes: Chunk[Byte] = boundary.closingBoundaryBytes
  }

  final case class Header(name: String, value: String) extends DecodingPart1AST {

    /**
     * The preposition is the first part of the header value before the first
     * semicolon. For example, the preposition of "text/html; charset=utf-8" is
     * "text/html".
     *
     * If there is no semicolon, the entire value is returned.
     *
     * @return
     */
    def preposition: String = value.split(';').head

    def fields: Map[String, String] =
      value
        .split(';')
        .map(_.trim)
        .flatMap { field =>
          val tokens = field.split('=')
          if (tokens.size > 1)
            Some(tokens(0) -> tokens.last.stripPrefix("\"").stripSuffix("\""))
          else Option.empty[(String, String)]
        }
        .toMap

    def toHeaders: Headers = Headers(name -> value)

    def bytes: Chunk[Byte] = Chunk.fromArray(s"$name: $value".getBytes(StandardCharsets.UTF_8))
  }

  object Header {

    private def makeField(name: String, value: String): String = s"""; ${name}="${value}""""

    private def makeField(name: String, value: Option[String]): String =
      value.map(makeField(name, _)).getOrElse("")

    def contentType(contentType: MediaType): Header =
      Header(
        "Content-Type",
        s"${contentType.fullType}${contentType.parameters.map { case (name, value) => s"; $name=$value" }.mkString("")}",
      )

    def contentDisposition(name: String, filename: Option[String] = None): Header =
      Header("Content-Disposition", s"""form-data${makeField("name", name)}${makeField("filename", filename)}""")

    def contentTransferEncoding(xferEncoding: ContentTransferEncoding): Header =
      Header("Content-Transfer-Encoding", xferEncoding.renderedValue.toString)

    def fromBytes(bytes: Array[Byte], encoding: Charset = StandardCharsets.UTF_8): Option[Header] = {
      val i = bytes.indexOf(':')

      if (i > -1) {
        bytes.splitAt(i) match {
          case (nameBytes, valueBytes) =>
            Some(Header(new String(nameBytes, encoding), new String(valueBytes.splitAt(1)._2, encoding).trim))
        }
      } else None
    }
  }

  final case class Content(bytes: Chunk[Byte]) extends DecodingPart2AST
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy