Skip to content

Commit

Permalink
feat: parsing and printing optimized (#26)
Browse files Browse the repository at this point in the history
* feat: parsing and printing optimized

* chore: Scala 2.12 compilation fixed

* chore: more tests and comments
  • Loading branch information
augi authored Jul 15, 2021
1 parent aab35bd commit 65d972f
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 5 deletions.
54 changes: 49 additions & 5 deletions core/src/main/scala/com/avast/scala/hashes/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,63 @@ import java.util.regex.Pattern
package object hashes {

private val hexAllowedCharactersRegex = Pattern.compile("[^0-9A-Fa-f]")
def hex2bytes(hex: String): Array[Byte] = hexbytesUnchecked(hexAllowedCharactersRegex.matcher(hex).replaceAll(""))
private def hexbytesUnchecked(hex: String): Array[Byte] = hex.sliding(2, 2).toArray.map(Integer.parseInt(_, 16).toByte)

/**
* Removes all the non-HEX characters from the input string and then transform the remaining HEX characters to byte array.
* So this method never throws an exception.
*/
def hex2bytes(hex: String): Array[Byte] = hex2bytesUnchecked(hexAllowedCharactersRegex.matcher(hex).replaceAll(""))

private def hex2bytesUnchecked(hex: String): Array[Byte] = {
val r = new Array[Byte](hex.length / 2)
var i = 0
while (i < (hex.length / 2)) {
r(i) = ((hexCharToInt(hex(i*2)) << 4) | hexCharToInt(hex(i*2+1))).toByte
i += 1
}
r
}

private def hexCharToInt(c: Character): Int = c match {
case c if c >= '0' && c <= '9' => c - '0'
case c if c >= 'a' && c <= 'f' => 10 + (c - 'a')
case c if c >= 'A' && c <= 'F' => 10 + (c - 'A')
case _ => throw new IllegalArgumentException
}

/**
* Removes all the non-HEX characters from the input string and then performs the check if the remaining characters
* could be decoded to expected bytes. If so, decodes them, returns None otherwise.
*/
def tryHex2bytes(maybeHex: String, expectedBytes: Int): Option[Array[Byte]] = {
val clean = hexAllowedCharactersRegex.matcher(maybeHex).replaceAll("")
if (clean.length == 2 * expectedBytes) Some(hexbytesUnchecked(clean)) else None
if (clean.length == 2 * expectedBytes) Some(hex2bytesUnchecked(clean)) else None
}

/**
* Encodes bytes to lower-case HEX string, optionally with a separator between bytes (so between every two characters).
*/
def bytes2hex(bytes: Array[Byte], sep: Option[String] = None): String =
sep match {
case None => bytes.map("%02x".format(_)).mkString
case _ => bytes.map("%02x".format(_)).mkString(sep.get)
case None =>
val sb = new StringBuilder(bytes.length * 2)
bytes.foreach(byteToHexChars(_, sb))
sb.toString()
case Some(s) =>
val sb = new StringBuilder(bytes.length * (2 + s.length))
bytes.foreach { b =>
byteToHexChars(b, sb)
sb.append(s)
}
if (sb.isEmpty) "" else sb.substring(0, sb.length - s.length)
}

private val hexAlphabet = Array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f')
private def byteToHexChars(b: Byte, sb: StringBuilder): Unit = {
sb.append(hexAlphabet((b >> 4 & 0x0f).toByte.toInt))
.append(hexAlphabet((b & 0x0f).toByte.toInt))
}

def base642bytes(base64: String): Array[Byte] = Base64.getDecoder.decode(base64)

def bytes2base64(bytes: Array[Byte]): String = Base64.getEncoder.encodeToString(bytes)
Expand Down
49 changes: 49 additions & 0 deletions core/src/test/scala/com/avast/scala/hashes/PackageTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.avast.scala.hashes

import org.junit.runner.RunWith
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import org.scalatestplus.junit.JUnitRunner

@RunWith(classOf[JUnitRunner])
class PackageTest extends AnyFlatSpec with Matchers {

Seq((Array(0, 25, 145, 255).map(_.toByte), Some("-"), "00-19-91-ff"),
(Array.emptyByteArray, Some("-"), ""),
(Array(0, 25, 145, 255).map(_.toByte), None, "001991ff")).foreach { case (bytes: Array[Byte], separator, expected) =>

it must s"convert ${bytes.mkString("Array(", ", ", ")")} separated with $separator to $expected" in {
bytes2hex(bytes, separator) shouldBe expected
}

}

Seq(
("TRU", 3, None),
("", 1, None),
(" TRU", 3, None),
(" ", 0, Some(Array.emptyByteArray)),
("001991FF", 3, None),
("001991FF", 5, None),
("001991FF", 4, Some(Array(0, 25, 145, 255).map(_.toByte)))).foreach { case (input, expectedBytes, expectedResult) =>
it must s"return expected result from tryHex2bytes for input '$input' string and expecting $expectedBytes bytes" in {
tryHex2bytes(input, expectedBytes) match {
case Some(value) => value shouldBe expectedResult.get
case None => expectedResult shouldBe None
}
}
}

Seq(
("001991FF", Array(0, 25, 145, 255).map(_.toByte)),
(" 001991FF ", Array(0, 25, 145, 255).map(_.toByte)),
("uu001991FFuu", Array(0, 25, 145, 255).map(_.toByte)),
("WFTF", Array(255).map(_.toByte)),
("uuuu", Array.emptyByteArray),
("", Array.emptyByteArray)).foreach { case (input, expectedResult) =>
it must s"return expected result from hex2bytes for input '$input' string" in {
hex2bytes(input) shouldBe expectedResult
}
}

}

0 comments on commit 65d972f

Please sign in to comment.