Skip to content

Commit

Permalink
Use an internal Base64 implementation.
Browse files Browse the repository at this point in the history
Fixes #34
  • Loading branch information
thesamet committed Aug 22, 2015
1 parent 13b2a0f commit 32c67e0
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 6 deletions.
7 changes: 5 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ lazy val projectReleaseSettings = Seq(
releasePublishArtifactsAction := {},
releasePublishArtifactsAction <<= releasePublishArtifactsAction.dependsOn(
PgpKeys.publishSigned,
clean,
publishLocal)
)

Expand All @@ -55,8 +56,10 @@ lazy val root =
lazy val runtime = project.in(file("scalapb-runtime")).settings(
projectReleaseSettings:_*)

lazy val compilerPlugin = project.in(file("compiler-plugin")).settings(
projectReleaseSettings:_*)
lazy val compilerPlugin = project.in(file("compiler-plugin"))
.dependsOn(runtime)
.settings(
projectReleaseSettings:_*)

lazy val proptest = project.in(file("proptest"))
.dependsOn(runtime, compilerPlugin)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -905,17 +905,17 @@ class ProtobufGenerator(val params: GeneratorParams) extends DescriptorPimps {
def generateFileDescriptor(file: FileDescriptor)(fp: FunctionalPrinter): FunctionalPrinter = {
// Encoding the file descriptor proto in base64. JVM has a limit on string literal to be up
// to 64k, so we chunk it into a sequence and combining in run time. The chunks are less
// than base64 to account for indentation and new lines.
// than 64k to account for indentation and new lines.
val clearProto = file.toProto.toBuilder.clearSourceCodeInfo.build
val base64: Seq[Seq[String]] = javax.xml.bind.DatatypeConverter.printBase64Binary(clearProto.toByteArray)
val base64: Seq[Seq[String]] = com.trueaccord.scalapb.Encoding.toBase64(clearProto.toByteArray)
.grouped(55000).map {
group =>
val lines = ("\"\"\"" + group).grouped(100).toSeq
lines.dropRight(1) :+ (lines.last + "\"\"\"")
}.toSeq
fp.add("lazy val descriptor: com.google.protobuf.Descriptors.FileDescriptor = {")
.add(" val proto = com.google.protobuf.DescriptorProtos.FileDescriptorProto.parseFrom(")
.add(" javax.xml.bind.DatatypeConverter.parseBase64Binary(Seq(")
.add(" com.trueaccord.scalapb.Encoding.fromBase64(Seq(")
.addGroupsWithDelimiter(",")(base64)
.add(" ).mkString))")
.add(" com.google.protobuf.Descriptors.FileDescriptor.buildFrom(proto, Array(")
Expand Down
4 changes: 3 additions & 1 deletion scalapb-runtime/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ name := "scalapb-runtime"

libraryDependencies ++= Seq(
"com.google.protobuf" % "protobuf-java" % "3.0.0-alpha-3",
"com.trueaccord.lenses" %% "lenses" % "0.4"
"com.trueaccord.lenses" %% "lenses" % "0.4",
"org.scalacheck" %% "scalacheck" % "1.12.4" % "test",
"org.scalatest" %% "scalatest" % "2.2.4" % "test"
)

unmanagedResourceDirectories in Compile += baseDirectory.value / "../protobuf"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.trueaccord.scalapb

import scala.collection.mutable

/** Utility functions to encode/decode byte arrays as Base64 strings.
*
* Used internally between the protocol buffer compiler and the runtime to encode
* messages.
*
* We could have used Apache Commons, but we would like to avoid an additional dependency.
* javax.xm.bind.DayaTypeConverter.parseBase64Binary is not available on Android. And the Java
* native java.util.Base64 is only available for Java 8...
*/
object Encoding {
private val alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="

def fromBase64(textInput: String): Array[Byte] = {
fromBase64Inner(textInput.filter(alphabet.contains(_: Char)))
}

private def fromBase64Inner(input: String): Array[Byte] = {
require(input.length % 4 == 0)
val lastEqualsIndex = input.indexOf('=')
val outputLength = (input.length * 3) / 4 - (
if (lastEqualsIndex > 0) (input.length() - input.indexOf('=')) else 0)
val builder = mutable.ArrayBuilder.make[Byte]
builder.sizeHint(outputLength)

for { i <- 0 until(input.length, 4) } {
val b = input.substring(i, i + 4).map(alphabet.indexOf(_: Char).toByte)
builder += ((b(0) << 2) | (b(1) >> 4)).toByte
if (b(2) < 64) {
builder += ((b(1) << 4) | (b(2) >> 2)).toByte
if (b(3) < 64) {
builder += ((b(2) << 6) | b(3)).toByte
}
}
}
builder.result()
}

def toBase64(in: Array[Byte]): String = {
val out = mutable.StringBuilder.newBuilder
var b: Int = 0
for { i <- 0 until (in.length, 3) } {
b = (in(i) & 0xFC) >> 2
out.append(alphabet(b))
b = (in(i) & 0x03) << 4
if (i + 1 < in.length) {
b |= (in(i + 1) & 0xF0) >> 4
out.append(alphabet(b))
b = (in(i + 1) & 0x0F) << 2
if (i + 2 < in.length) {
b |= (in(i + 2) & 0xC0) >> 6
out.append(alphabet(b))
b = in(i + 2) & 0x3F
out.append(alphabet(b))
} else {
out.append(alphabet(b))
out.append('=')
}
} else {
out.append(alphabet(b))
out.append("==")
}
}
out.result()
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.trueaccord.scalapb

import org.scalatest._
import org.scalatest.prop.GeneratorDrivenPropertyChecks

class EncodingSpec extends PropSpec with GeneratorDrivenPropertyChecks with Matchers {
property("fromBase64 is the inverse of toBase64") {
forAll {
b: Array[Byte] =>
Encoding.fromBase64(Encoding.toBase64(b)) should be(b)
}
}

property("fromBase64 is compatible with javax.printBase64") {
forAll {
b: Array[Byte] =>
Encoding.fromBase64(javax.xml.bind.DatatypeConverter.printBase64Binary(b)) should be(b)
}
}

property("toBase64 is compatible with javax.parseBase64") {
forAll {
b: Array[Byte] =>
javax.xml.bind.DatatypeConverter.parseBase64Binary(
Encoding.toBase64(b)) should be(b)
}
}
}

0 comments on commit 32c67e0

Please sign in to comment.