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

scalapb.textformat.TextFormatUtils.scala Maven / Gradle / Ivy

The newest version!
package scalapb.textformat

import java.math.BigInteger

import com.google.protobuf.ByteString
import scalapb.TextFormatError

import scala.collection.mutable

private[scalapb] object TextFormatUtils {
  import Constants._

  sealed trait ByteParsingState extends Any

  case object Default extends ByteParsingState

  case object EscapeMode extends ByteParsingState

  case object Hex0 extends ByteParsingState

  case class Hex1(b: Int) extends AnyVal with ByteParsingState

  case class Octal1(b: Int) extends AnyVal with ByteParsingState

  case class Octal2(b: Int) extends AnyVal with ByteParsingState

  case class Error(s: String) extends AnyVal with ByteParsingState

  private val HEXDIGIT = "0123456789abcdefABCDEF"

  def isHexDigit(c: Char) = HEXDIGIT.contains(c)

  private def digitValue(c: Byte) = {
    if ('0' <= c && c <= '9')
      (c - '0')
    else if ('a' <= c && c <= 'z')
      (c - 'a' + 10)
    else
      (c - 'A' + 10)
  }

  def escapeBytes(bytes: ByteString): String = {
    val sb = new mutable.StringBuilder()
    bytes.foreach {
      case CH_SLASH_A => sb.append("\\a")
      case CH_SLASH_B => sb.append("\\b")
      case CH_SLASH_F => sb.append("\\f")
      case CH_SLASH_N => sb.append("\\n")
      case CH_SLASH_R => sb.append("\\r")
      case CH_SLASH_T => sb.append("\\t")
      case CH_SLASH_V => sb.append("\\v")
      case CH_SLASH   => sb.append("\\\\")
      case CH_SQ      => sb.append("\\\'")
      case CH_DQ      => sb.append("\\\"")
      case b if b >= 0x20 =>
        sb.append(b.toChar)
      case b =>
        sb.append('\\')
        sb.append((48 + ((b >>> 6) & 3)).toChar)
        sb.append((48 + ((b >>> 3) & 7)).toChar)
        sb.append((48 + (b & 7)).toChar)
    }
    sb.result()
  }

  // JavaConversions is removed in 2.13. Using JavaConverters doesn't work for us; asScala is not available
  // on the pure Scala implementation of ByteString.
  implicit class JavaConversions(val iter: java.lang.Iterable[java.lang.Byte]) extends AnyVal {
    import scala.jdk.CollectionConverters._
    def foldLeft[B](z: B)(op: (B, java.lang.Byte) => B) = iter.asScala.foldLeft(z)(op)

    def foreach[U](f: java.lang.Byte => U): Unit = iter.asScala.foreach(f)
  }

  def unescapeBytes(
      charString: String
  ): Either[TextFormatError, ByteString] with Product with Serializable = {
    val input: ByteString = ByteString.copyFromUtf8(charString)
    val result            = mutable.ArrayBuilder.make[Byte]
    result.sizeHint(input.size)

    def defaultHandle(b: Byte) = {
      if (b == CH_SLASH) EscapeMode
      else {
        result += b
        Default
      }
    }

    val endState = input.foldLeft[ByteParsingState](Default) { (state, b) =>
      (state, b) match {
        case (e: Error, _)                           => e
        case (Default, b)                            => defaultHandle(b)
        case (EscapeMode, b) if b >= '0' && b <= '7' => Octal1(digitValue(b))
        case (EscapeMode, CH_A)                      => result += 7; Default
        case (EscapeMode, CH_B)                      => result += '\b'; Default
        case (EscapeMode, CH_F)                      => result += '\f'; Default
        case (EscapeMode, CH_N)                      => result += '\n'; Default
        case (EscapeMode, CH_R)                      => result += '\r'; Default
        case (EscapeMode, CH_T)                      => result += '\t'; Default
        case (EscapeMode, CH_V)                      => result += 11; Default
        case (EscapeMode, CH_SLASH)                  => result += '\\'; Default
        case (EscapeMode, CH_SQ)                     => result += '\''; Default
        case (EscapeMode, CH_DQ)                     => result += '\"'; Default
        case (EscapeMode, CH_QM)                     => result += '?'; Default
        case (EscapeMode, CH_X)                      => Hex0
        case (EscapeMode, _) => Error("Invalid escape sequence: " + b.toChar)
        case (Octal1(i), b) if b >= '0' && b <= '7' =>
          Octal2(i * 8 + digitValue(b))
        case (Octal1(i), b) =>
          result += i.toByte
          defaultHandle(b)
        case (Octal2(i), b) if b >= '0' && b <= '7' =>
          result += (i * 8 + digitValue(b)).toByte
          Default
        case (Octal2(i), b) =>
          result += i.toByte
          defaultHandle(b)
        case (Hex0, b) if (isHexDigit(b.toChar)) => Hex1(digitValue(b))
        case (Hex0, _)                           => Error("'\\x' with no digits")
        case (Hex1(i), b) if (isHexDigit(b.toChar)) =>
          result += (16 * i + digitValue(b)).toByte
          Default
        case (Hex1(i), b) =>
          result += i.toByte
          defaultHandle(b)
      }
    }

    endState match {
      case Error(e)   => Left(TextFormatError(e))
      case Hex0       => Left(TextFormatError("'\\x' with no digits"))
      case EscapeMode => Left(TextFormatError("Invalid escape sequence '\\' at end of string."))
      case Hex1(i) =>
        result += i.toByte
        Right(ByteString.copyFrom(result.result()))
      case Octal1(i) =>
        result += i.toByte
        Right(ByteString.copyFrom(result.result()))
      case Octal2(i) =>
        result += i.toByte
        Right(ByteString.copyFrom(result.result()))
      case Default =>
        Right(ByteString.copyFrom(result.result()))
    }
  }

  def escapeText(input: String): String =
    escapeBytes(ByteString.copyFromUtf8(input))

  def unescapeText(input: String): Either[TextFormatError, String] = for {
    bytes <- unescapeBytes(input)
    _     <- if (bytes.isValidUtf8()) Right(()) else Left(TextFormatError(s"Invalid UTF8: $input"))
  } yield bytes.toStringUtf8()

  /** Convert an unsigned 32-bit integer to a string. */
  def unsignedToString(value: Int): String = {
    if (value >= 0) java.lang.Integer.toString(value)
    else java.lang.Long.toString(value & 0x00000000ffffffffL)
  }

  /** Convert an unsigned 64-bit integer to a string. */
  def unsignedToString(value: Long): String = {
    if (value >= 0) java.lang.Long.toString(value)
    else BigInteger.valueOf(value & 0x7fffffffffffffffL).setBit(63).toString
  }

  /** Escape double quotes and backslashes in a String for unicode output of a message.
    */
  def escapeDoubleQuotesAndBackslashes(input: String): String = {
    input.replace("\\", "\\\\").replace("\"", "\\\"")
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy