├── .gitignore ├── .jot ├── .travis.yml ├── LICENSE ├── README.md ├── build.sbt ├── notes ├── 0.1.0.markdown ├── 0.1.1.markdown ├── 0.2.0.markdown └── about.markdown ├── project ├── build.properties └── plugins.sbt └── src ├── main ├── ls │ ├── 0.1.0.json │ └── 0.1.1.json └── scala │ ├── alphabets.scala │ ├── decode.scala │ ├── encode.scala │ ├── input.scala │ └── package.scala └── test ├── java └── base64 │ └── JavaTest.java └── scala └── base64 ├── Base64Benchmark.scala ├── Base64Spec.scala └── bench.scala /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /.jot: -------------------------------------------------------------------------------- 1 | apache tests https://github.com/apache/commons-codec/blob/1_5_RELEASE/src/test/java/org/apache/commons/codec/binary/Base64Test.java 2 | haskell tests https://github.com/bos/base64-bytestring/blob/master/tests/Tests.hs 3 | js tests https://github.com/davidchambers/Base64.js/blob/master/test/base64.coffee 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | before_script: 3 | - "echo $JAVA_OPTS" 4 | - "export JAVA_OPTS='-Xmx512m -Xss1M -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256M'" 5 | language: scala 6 | scala: 7 | - 2.10.4 8 | - 2.11.5 9 | jdk: 10 | - openjdk6 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2014 Doug Tangren 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # base64 2 | 3 | [![Build Status](https://travis-ci.org/softprops/base64.svg)](https://travis-ci.org/softprops/base64) 4 | 5 | > the 64th base of rfc4648 6 | 7 | This is a library for base64 encoding and decoding raw data. 8 | 9 | ## Install 10 | 11 | Via the copy and paste method 12 | 13 | ```scala 14 | resolvers += "softprops-maven" at "http://dl.bintray.com/content/softprops/maven" 15 | 16 | libraryDependencies += "me.lessis" %% "base64" % "0.2.0" 17 | ``` 18 | 19 | Via [a more civilized method](https://github.com/softprops/ls#readme) which will do the same without all the manual work. 20 | 21 | > ls-install base64 22 | 23 | _Note_ If you are a [bintray-sbt](https://github.com/softprops/bintray-sbt#readme) user you can optionally specify the resolver as 24 | 25 | ```scala 26 | resolvers += bintray.Opts.resolver.repo("softprops", "maven") 27 | ``` 28 | 29 | ## Usage 30 | 31 | This library encodes and decodes Byte Arrays but exposes a [typeclass interface](https://github.com/softprops/base64/blob/master/src/main/scala/input.scala#L8-L10) for providing input defined as 32 | 33 | ```scala 34 | trait Input[T] { 35 | def bytes: Array[Byte] 36 | } 37 | ``` 38 | 39 | Instances of this typeclass are defined for `java.nio.ByteBuffer`, `String`, `(String, java.nio.charset.Charset)`, and 40 | `Array[Bytes]`. 41 | 42 | ### Standard Encoding 43 | 44 | To base64 encode input simply invoke the `Encode` objects `apply` method 45 | 46 | ```scala 47 | base64.Encode("Man") 48 | ``` 49 | 50 | This returns a Byte Array. To make this output human readable, you may wish to create a String from its output. 51 | 52 | ### URL-Safe Encoding 53 | 54 | When working with web applications its a common need to base64 encode information in a urlsafe way. Do do so with this library 55 | just invoke `urlSafe` with input on the `Encode` object 56 | 57 | ```scala 58 | new String(base64.Encode.urlSafe("hello world?")) // aGVsbG8gd29ybGQ_ 59 | ``` 60 | 61 | ### Multiline Encoding 62 | 63 | Fixing the width of base64 encoded data is, in some cases, a desireble property. In these cases, set the `multiline` flag to true when encoding. 64 | 65 | ```scala 66 | val in = "Base64 is a group of similar binary-to-text encoding schemes that represent binary data in an ASCII string format by translating it into a radix-64 representation. The term Base64 originates from a specific MIME content transfer encoding." 67 | 68 | new String(base64.Encode(in, multiline = true)) 69 | ``` 70 | 71 | will produce 72 | 73 | ``` 74 | QmFzZTY0IGlzIGEgZ3JvdXAgb2Ygc2ltaWxhciBiaW5hcnktdG8tdGV4dCBlbmNvZGluZyBzY2hl 75 | bWVzIHRoYXQgcmVwcmVzZW50IGJpbmFyeSBkYXRhIGluIGFuIEFTQ0lJIHN0cmluZyBmb3JtYXQg 76 | YnkgdHJhbnNsYXRpbmcgaXQgaW50byBhIHJhZGl4LTY0IHJlcHJlc2VudGF0aW9uLiBUaGUgdGVy 77 | bSBCYXNlNjQgb3JpZ2luYXRlcyBmcm9tIGEgc3BlY2lmaWMgTUlNRSBjb250ZW50IHRyYW5zZmVy 78 | IGVuY29kaW5nLg== 79 | ``` 80 | 81 | ### Omitting padding 82 | 83 | You can omit padding from the output of encodings by setting `pad` option to false 84 | 85 | This will have the following effect on the results 86 | 87 | 88 | With padding 89 | 90 | ```scala 91 | new String(base64.Encode("paddington")) // cGFkZGluZ3Rvbg== 92 | ``` 93 | 94 | Without padding 95 | 96 | ```scala 97 | new String(base64.Encode("paddington", pad = false)) // cGFkZGluZ3Rvbg 98 | ``` 99 | 100 | ### Decoding 101 | 102 | A dual for each is provided with the `Decode` object. 103 | 104 | ```scala 105 | new String(base64.Decode.urlSafe(base64.Encode.urlSafe("hello world?"))) // hello world? 106 | ``` 107 | 108 | ## Why 109 | 110 | Chances are you probably need a base64 codec. 111 | 112 | Chances are you probably don't need everything that came with the library you use to base64 encode data. 113 | 114 | This library aims to only do one thing. base64 _. That's it. 115 | 116 | A seconday goal was to fully understand [rfc4648](http://www.ietf.org/rfc/rfc4648.txt) from first principals. Implementation is a good learning tool. You should try it. 117 | 118 | ## Performance 119 | 120 | Performance really depends on your usecase, _no matter library you use_. An attempt was made to compare 121 | the encoding and decoding performance with the same input data against apache commons-codec base64 and 122 | netty 4.0.7.final base64. 123 | 124 | For encoding and decoding I found the following general repeating performance patterns 125 | when testing [15,000 runs](https://github.com/softprops/base64/blob/master/src/test/scala/base64/bench.scala#L53) for each library for each operation. 126 | 127 | ``` 128 | enc apache commons (byte arrays) took 97 ms 129 | enc netty (byte buf) took 95 ms 130 | enc ours (byte arrays) took 121 ms 131 | dec apache commons (byte arrays) took 77 ms 132 | dec netty (byte buf) took 171 ms 133 | dec ours (byte arrays) took 85 ms 134 | ``` 135 | 136 | Take this with a grain of salt. None of these will be the performance bottle neck of your application. This was 137 | just a simple measurement test to make sure this library was not doing something totally naive. 138 | 139 | ### inspiration and learning 140 | 141 | taken from 142 | 143 | * [Robert Harder's public domain](http://iharder.sourceforge.net/current/java/base64/) 144 | * [netty base64](https://github.com/netty/netty/tree/master/codec/src/main/java/io/netty/handler/codec/base64) 145 | * [haskell base64](https://github.com/bos/base64-bytestring/tree/master/Data/ByteString) 146 | * [@tototoshi](https://github.com/tototoshi/scala-base64) 147 | 148 | Doug Tangren (softprops) 2013-2014 149 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | organization := "me.lessis" 2 | 3 | name := "base64" 4 | 5 | version := "0.2.1" 6 | 7 | licenses := Seq( 8 | ("MIT", url(s"https://github.com/softprops/${name.value}/blob/${version.value}/LICENSE"))) 9 | 10 | homepage := Some(url(s"https://github.com/softprops/${name.value}/#readme")) 11 | 12 | scalacOptions += Opts.compile.deprecation 13 | 14 | crossScalaVersions := Seq("2.10.4", "2.11.5", "2.12.1") 15 | 16 | scalaVersion := crossScalaVersions.value.last 17 | 18 | libraryDependencies ++= Seq( 19 | "org.scalatest" %% "scalatest" % "3.0.0" % "test", 20 | "commons-codec" % "commons-codec" % "1.9" % "test", 21 | "io.netty" % "netty-codec" % "4.0.23.Final" % "test") 22 | 23 | bintraySettings 24 | 25 | bintray.Keys.packageLabels in bintray.Keys.bintray := Seq("base64", "encoding", "rfc4648") 26 | 27 | lsSettings 28 | 29 | LsKeys.tags in LsKeys.lsync := (bintray.Keys.packageLabels in bintray.Keys.bintray).value 30 | 31 | externalResolvers in LsKeys.lsync := (resolvers in bintray.Keys.bintray).value 32 | 33 | cappiSettings 34 | 35 | pomExtra := ( 36 | 37 | git@github.com:softprops/{name.value}.git 38 | scm:git:git@github.com:softprops/{name.value}.git 39 | 40 | 41 | 42 | softprops 43 | Doug Tangren 44 | https://github.com/softprops 45 | 46 | ) 47 | -------------------------------------------------------------------------------- /notes/0.1.0.markdown: -------------------------------------------------------------------------------- 1 | ## Initial release 2 | 3 | This is the first release of [base64](https://github.com/softprops/base64/#readme), a library for encoding raw data into its radix-64 representation. 4 | 5 | This library was made with ❤ in the spirit of [only doing one thing](https://github.com/softprops/base64/#why) without extra fanfare and doing it with [comparable performance](https://github.com/softprops/base64/#performance) to alternatives which all carry extra baggage. 6 | 7 | Basic usage is as simple as 8 | 9 | val payload = "Hadoop online... Caches warmed... Engage!" 10 | base64.Decode(base64.Encode(payload)) 11 | .right.map(_.sameElements(payload)) 12 | 13 | For more engagement, see the project's [readme](https://github.com/softprops/base64/#readme) 14 | -------------------------------------------------------------------------------- /notes/0.1.1.markdown: -------------------------------------------------------------------------------- 1 | ## Changes 2 | 3 | - Made `Input` type class instances somewhat more accessible for java users by using vals instead of object definitions. 4 | - Dropped support for Scala 2.9.3 added support for scala 2.11.* 5 | -------------------------------------------------------------------------------- /notes/0.2.0.markdown: -------------------------------------------------------------------------------- 1 | # enhancements 2 | 3 | Added ability to omit padding characters when encoding 4 | 5 | // with 6 | new String(base64.Encode("paddington")) // cGFkZGluZ3Rvbg== 7 | // without 8 | new String(base64.Encode("paddington", pad = false)) // cGFkZGluZ3Rvbg 9 | 10 | Note that padding has no effect when encoding 11 | 12 | // with 13 | base64.Decode(base64.Encode("paddington")) 14 | .right.map(new String(_)) // paddington 15 | // without 16 | base64.Decode(base64.Encode("paddington", pad = false)) 17 | .right.map(new String(_)) // paddington 18 | 19 | Note that padding was previously expected. Decoding now gracefully handles its omission 20 | -------------------------------------------------------------------------------- /notes/about.markdown: -------------------------------------------------------------------------------- 1 | [base64](https://github.com/softprops/base64#readme) is a pure scala library for covering the the 64th base of [rfc4648](http://www.ietf.org/rfc/rfc4648.txt) 2 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.7 -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers += Resolver.url( 2 | "bintray-sbt-plugin-releases", 3 | url("http://dl.bintray.com/content/sbt/sbt-plugin-releases"))( 4 | Resolver.ivyStylePatterns) 5 | 6 | addSbtPlugin("me.lessis" % "bintray-sbt" % "0.1.2") 7 | 8 | addSbtPlugin("me.lessis" % "ls-sbt" % "0.1.3") 9 | 10 | addSbtPlugin("me.lessis" % "cappi" % "0.1.1") 11 | 12 | -------------------------------------------------------------------------------- /src/main/ls/0.1.0.json: -------------------------------------------------------------------------------- 1 | { 2 | "organization" : "me.lessis", 3 | "name" : "base64", 4 | "version" : "0.1.0", 5 | "description" : "base64", 6 | "site" : "https://github.com/softprops/base64/#readme", 7 | "tags" : [ "base64", "encoding", "rfc4648" ], 8 | "docs" : "", 9 | "resolvers" : [ "http://dl.bintray.com/content/softprops/maven" ], 10 | "dependencies" : [ ], 11 | "scalas" : [ "2.9.3", "2.10.2" ], 12 | "licenses" : [ { 13 | "name" : "MIT", 14 | "url" : "https://github.com/softprops/base64/blob/0.1.0/LICENSE" 15 | } ], 16 | "sbt" : false 17 | } -------------------------------------------------------------------------------- /src/main/ls/0.1.1.json: -------------------------------------------------------------------------------- 1 | { 2 | "organization" : "me.lessis", 3 | "name" : "base64", 4 | "version" : "0.1.1", 5 | "description" : "base64", 6 | "site" : "https://github.com/softprops/base64/#readme", 7 | "tags" : [ "base64", "encoding", "rfc4648" ], 8 | "docs" : "", 9 | "resolvers" : [ "http://dl.bintray.com/content/softprops/maven" ], 10 | "dependencies" : [ ], 11 | "scalas" : [ "2.10.4", "2.11.2" ], 12 | "licenses" : [ { 13 | "name" : "MIT", 14 | "url" : "https://github.com/softprops/base64/blob/0.1.1/LICENSE" 15 | } ], 16 | "sbt" : false 17 | } -------------------------------------------------------------------------------- /src/main/scala/alphabets.scala: -------------------------------------------------------------------------------- 1 | package base64 2 | 3 | trait Alphabet { 4 | protected val Base = 5 | (('A' to 'Z') ++ ('a' to 'z') ++ ('0' to '9')).map(_.toByte) 6 | def values: IndexedSeq[Byte] 7 | def reversed: Array[Byte] 8 | } 9 | 10 | /** Standard Base64 encoding as described in second 4 of 11 | * 12 | */ 13 | object StdAlphabet extends Alphabet { 14 | val values = Base ++ Vector('+', '/').map(_.toByte) 15 | val reversed: Array[Byte] = Array( 16 | -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8 17 | -5,-5, // Whitespace: Tab and Linefeed 18 | -9,-9, // Decimal 11 - 12 19 | -5, // Whitespace: Carriage Return 20 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26 21 | -9,-9,-9,-9,-9, // Decimal 27 - 31 22 | -5, // Whitespace: Space 23 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42 24 | 62, // Plus sign at decimal 43 25 | -9,-9,-9, // Decimal 44 - 46 26 | 63, // Slash at decimal 47 27 | 52,53,54,55,56,57,58,59,60,61, // Numbers zero through nine 28 | -9,-9,-9, // Decimal 58 - 60 29 | -1, // Equals sign at decimal 61 30 | -9,-9,-9, // Decimal 62 - 64 31 | 0,1,2,3,4,5,6,7,8,9,10,11,12,13, // Letters 'A' through 'N' 32 | 14,15,16,17,18,19,20,21,22,23,24,25, // Letters 'O' through 'Z' 33 | -9,-9,-9,-9,-9,-9, // Decimal 91 - 96 34 | 26,27,28,29,30,31,32,33,34,35,36,37,38, // Letters 'a' through 'm' 35 | 39,40,41,42,43,44,45,46,47,48,49,50,51, // Letters 'n' through 'z' 36 | -9,-9,-9,-9,-9 // Decimal 123 - 127 37 | ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139 38 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 39 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 40 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 41 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 42 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 43 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 44 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 45 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 46 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 47 | ) 48 | } 49 | 50 | /** Base64-like encoding that is URL-safe as described in the Section 5 of 51 | * 52 | */ 53 | object URLSafeAlphabet extends Alphabet { 54 | val values = Base ++ Vector('-', '_').map(_.toByte) 55 | val reversed: Array[Byte] = Array( 56 | -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8 57 | -5,-5, // Whitespace: Tab and Linefeed 58 | -9,-9, // Decimal 11 - 12 59 | -5, // Whitespace: Carriage Return 60 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26 61 | -9,-9,-9,-9,-9, // Decimal 27 - 31 62 | -5, // Whitespace: Space 63 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42 64 | -9, // Plus sign at decimal 43 65 | -9, // Decimal 44 66 | 62, // Minus sign at decimal 45 67 | -9, // Decimal 46 68 | -9, // Slash at decimal 47 69 | 52,53,54,55,56,57,58,59,60,61, // Numbers zero through nine 70 | -9,-9,-9, // Decimal 58 - 60 71 | -1, // Equals sign at decimal 61 72 | -9,-9,-9, // Decimal 62 - 64 73 | 0,1,2,3,4,5,6,7,8,9,10,11,12,13, // Letters 'A' through 'N' 74 | 14,15,16,17,18,19,20,21,22,23,24,25, // Letters 'O' through 'Z' 75 | -9,-9,-9,-9, // Decimal 91 - 94 76 | 63, // Underscore at decimal 95 77 | -9, // Decimal 96 78 | 26,27,28,29,30,31,32,33,34,35,36,37,38, // Letters 'a' through 'm' 79 | 39,40,41,42,43,44,45,46,47,48,49,50,51, // Letters 'n' through 'z' 80 | -9,-9,-9,-9,-9 // Decimal 123 - 127 81 | ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139 82 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 83 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 84 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 85 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 86 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 87 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 88 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 89 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 90 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 91 | ) 92 | } 93 | -------------------------------------------------------------------------------- /src/main/scala/decode.scala: -------------------------------------------------------------------------------- 1 | package base64 2 | 3 | import java.util.Arrays 4 | 5 | object Decode { 6 | 7 | val Empty = Array.empty[Byte] 8 | 9 | sealed trait Failure 10 | case class InvalidByte(index: Int, dec: Int) extends Failure 11 | 12 | def urlSafe[T : Input](in: T) = 13 | decodeWith(URLSafeAlphabet)(in) 14 | 15 | def apply[T : Input](in: T) = 16 | decodeWith(StdAlphabet)(in) 17 | 18 | def decodeWith[T : Input]( 19 | alphabet: Alphabet)(ins: T): Either[Failure, Array[Byte]] = { 20 | val in = Input(ins) match { 21 | case in if in.length % 4 == 0 => 22 | in 23 | case p => 24 | def concat(a: Array[Byte], b: Array[Byte]): Array[Byte] = { 25 | val res = new Array[Byte](a.length + b.length) 26 | System.arraycopy(a, 0, res, 0, a.length) 27 | System.arraycopy(b, 0, res, a.length, b.length) 28 | res 29 | } 30 | // if padding was omited, fill it in ourselves 31 | concat(p, Array.fill(p.length % 4)(Pad)) 32 | } 33 | val len = in.length 34 | val len34 = len * 3 / 4 35 | val out = new Array[Byte](len34) 36 | val b4 = new Array[Byte](4) 37 | val index = alphabet.reversed 38 | val readBounds = len 39 | 40 | def read( 41 | at: Int = 0, 42 | b4Posn: Int = 0, 43 | outOffset: Int = 0 44 | ): Either[Failure, Int] = if (at >= readBounds) Right(outOffset) else { 45 | val sbiCrop = (in(at) & 0x7f).toByte // Only the low seven bits 46 | val sbiDecode = index(sbiCrop) 47 | val nextByte = at + 1 48 | if (sbiDecode >= WhiteSpaceEnc) { 49 | if (sbiDecode >= EqEnc) { 50 | b4.update(b4Posn, sbiCrop) 51 | val nextB4Posn = b4Posn + 1 52 | if (nextB4Posn > 3) { 53 | val cnt = dec4to3( 54 | b4, 0, out, outOffset, index 55 | ) 56 | val curOffset = outOffset + cnt 57 | if (sbiCrop == Pad) Right(curOffset) else read( 58 | nextByte, 0, curOffset 59 | ) 60 | } else read(nextByte, nextB4Posn, outOffset) 61 | } else read(nextByte, b4Posn, outOffset) 62 | } else Left(InvalidByte(at, index(at) & 0xFF)) 63 | } 64 | if (len < 4) Right(Empty) else read().right.map { 65 | case len => 66 | if (len == 1 && out(0) == -1) /*all padding*/ Empty 67 | else Arrays.copyOf(out, len) 68 | } 69 | } 70 | 71 | private def dec4to3( 72 | in: Array[Byte], 73 | inOffset: Int, 74 | out: Array[Byte], 75 | outOffset: Int, 76 | index: Array[Byte] 77 | ): Int = 78 | if (in(inOffset + 2) == Pad) { // Dk== 79 | val outBuff = ((index(in(inOffset)) & 0xFF) << 18) | 80 | ((index(in(inOffset + 1)) & 0xFF ) << 12) 81 | out.update(outOffset, (outBuff >>> 16).toByte) 82 | 1 83 | } else if (in(inOffset + 3) == Pad) { // DkL= 84 | val outBuff = ((index(in(inOffset)) & 0xFF) << 18) | 85 | ((index(in(inOffset + 1)) & 0xFF) << 12) | 86 | ((index(in(inOffset + 2)) & 0xFF) << 6) 87 | out.update(outOffset, (outBuff >>> 16).toByte) 88 | out.update(outOffset + 1, (outBuff >>> 8).toByte) 89 | 2 90 | } else { // DkLE 91 | val outBuff = ((index(in(inOffset)) & 0xFF) << 18) | 92 | ((index(in(inOffset + 1)) & 0xFF) << 12) | 93 | ((index(in(inOffset + 2)) & 0xFF) << 6) | 94 | ((index(in(inOffset + 3)) & 0xFF)) 95 | out.update(outOffset, (outBuff >> 16).toByte) 96 | out.update(outOffset + 1, (outBuff >> 8).toByte) 97 | out.update(outOffset + 2, (outBuff).toByte) 98 | 3 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/scala/encode.scala: -------------------------------------------------------------------------------- 1 | package base64 2 | 3 | import java.util.Arrays 4 | 5 | /** Base64 encodings. This implementation does not support line breaks */ 6 | object Encode { 7 | 8 | /** Encodes an array of bytes into a base64 encoded string 9 | * which accounts for url encoding provisions */ 10 | def urlSafe[T : Input](in: T, multiline: Boolean = false, pad: Boolean = true) = 11 | encodeWith(URLSafeAlphabet)(in, multiline, pad) 12 | 13 | /** Encodes an array of bytes into a base64 encoded string 14 | * */ 15 | def apply[T : Input](in: T, multiline: Boolean = false, pad: Boolean = true) = 16 | encodeWith(StdAlphabet)(in, multiline, pad) 17 | 18 | def encodeWith[T : Input]( 19 | alphabet: Alphabet) 20 | (ins: T, multiline: Boolean = false, pad: Boolean = true): Array[Byte] = { 21 | val in = Input(ins) 22 | val index = alphabet.values 23 | val len = in.size 24 | val len2 = len - 2 25 | val estimate = (len / 3) * 4 + (if (len % 3 > 0) 4 else 0) match { 26 | case est => if (multiline) est + (est / MaxLine) else est 27 | } 28 | val out = new Array[Byte](estimate) 29 | 30 | @annotation.tailrec 31 | def write(d: Int = 0, e: Int = 0, col: Int = 0): (Int, Int) = 32 | if (d >= len2) (d, e) 33 | else { 34 | enc3to4(in, d, 3, out, e, index, pad) 35 | if (multiline && col + 4 >= MaxLine) { 36 | out.update(e + 4, NewLine) 37 | write(d + 3, e + 5, 0) 38 | } else write(d + 3, e + 4, col + 4) 39 | } 40 | 41 | val (d, e) = write() 42 | val fe = // extra padding 43 | if (d < len) { 44 | val updated = enc3to4(in, d, len - d, out, e, index, pad) 45 | e + updated 46 | } else e 47 | if (fe < out.size - 1) Arrays.copyOf(out, fe) else out 48 | } 49 | 50 | private def enc3to4( 51 | in: Array[Byte], 52 | inOffset: Int, 53 | numSigBytes: Int, 54 | out: Array[Byte], 55 | outOffset: Int, 56 | index: IndexedSeq[Byte], 57 | pad: Boolean): Int = { 58 | 59 | // 1 2 3 60 | // 01234567890123456789012345678901 Bit position 61 | // --------000000001111111122222222 Array position from threeBytes 62 | // --------| || || || | Six bit groups to index ALPHABET 63 | // >>18 >>12 >> 6 >> 0 Right shift necessary 64 | // 0x3f 0x3f 0x3f Additional AND 65 | 66 | // Create buffer with zero-padding if there are only one or two 67 | // significant bytes passed in the array. 68 | // We have to shift left 24 in order to flush out the 1's that appear 69 | // when Java treats a value as negative that is cast from a byte to an int. 70 | val inBuff = (if (numSigBytes > 0) (in(inOffset) << 24) >>> 8 else 0) | 71 | (if (numSigBytes > 1) (in(inOffset + 1) << 24) >>> 16 else 0) | 72 | (if (numSigBytes > 2) (in(inOffset + 2) << 24) >>> 24 else 0) 73 | (numSigBytes: @annotation.switch) match { 74 | case 3 => 75 | out.update(outOffset, index(inBuff >>> 18)) 76 | out.update(outOffset + 1, index(inBuff >>> 12 & EncMask)) 77 | out.update(outOffset + 2, index(inBuff >>> 6 & EncMask)) 78 | out.update(outOffset + 3, index(inBuff & EncMask)) 79 | 4 80 | case 2 => 81 | out.update(outOffset, index(inBuff >>> 18)) 82 | out.update(outOffset + 1, index(inBuff >>> 12 & EncMask)) 83 | out.update(outOffset + 2, index(inBuff >>> 6 & EncMask)) 84 | if (pad) { 85 | out.update(outOffset + 3, Pad) 86 | 4 87 | } else 3 88 | case 1 => 89 | out.update(outOffset, index(inBuff >>> 18)) 90 | out.update(outOffset + 1, index(inBuff >>> 12 & EncMask)) 91 | if (pad) { 92 | out.update(outOffset + 2, Pad) 93 | out.update(outOffset + 3, Pad) 94 | 4 95 | } else 2 96 | case _ => 97 | 0 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/scala/input.scala: -------------------------------------------------------------------------------- 1 | package base64 2 | 3 | import java.nio.ByteBuffer 4 | import java.nio.charset.Charset 5 | 6 | @annotation.implicitNotFound( 7 | msg = "base64 Input[T] type class instance for type ${T} not found") 8 | trait Input[T] { 9 | def apply(t: T): Array[Byte] 10 | } 11 | 12 | object Input { 13 | private[this] val utf8 = Charset.forName("UTF-8") 14 | 15 | implicit val ByteBuffers: Input[ByteBuffer] = 16 | new Input[ByteBuffer] { 17 | def apply(in: ByteBuffer) = in.array 18 | } 19 | 20 | implicit val Bytes: Input[Array[Byte]] = 21 | new Input[Array[Byte]] { 22 | def apply(in: Array[Byte]) = in 23 | } 24 | 25 | implicit val Utf8Str: Input[String] = 26 | new Input[String] { 27 | def apply(in: String) = 28 | Str(in, utf8) 29 | } 30 | 31 | implicit val Str: Input[(String, Charset)] = 32 | new Input[(String, Charset)] { 33 | def apply(in: (String, Charset)) = 34 | Bytes(in._1.getBytes(in._2.name())) 35 | } 36 | 37 | def apply[T: Input](in: T) = implicitly[Input[T]].apply(in) 38 | } 39 | -------------------------------------------------------------------------------- /src/main/scala/package.scala: -------------------------------------------------------------------------------- 1 | package object base64 { 2 | val Pad: Byte = '=' 3 | val WhiteSpaceEnc = -5 4 | val EqEnc = -1 5 | val EncMask = 0x3f 6 | val MaxLine = 76 7 | val NewLine: Byte = '\n' 8 | } 9 | -------------------------------------------------------------------------------- /src/test/java/base64/JavaTest.java: -------------------------------------------------------------------------------- 1 | package base64; 2 | 3 | import scala.util.Either; 4 | 5 | public class JavaTest { 6 | public static void main(String[] args) { 7 | // type class instanes 8 | Input strs = Input$.MODULE$.Utf8Str(); 9 | Input bytes = Input$.MODULE$.Bytes(); 10 | // to and fro 11 | Either result = 12 | Decode.apply( 13 | Encode.apply("test", false, false, strs), 14 | bytes); 15 | // print result 16 | if (result.isRight()) { 17 | System.out.println( 18 | String.format("encoded then decoded '%s'", 19 | new String(result.right().get()))); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/test/scala/base64/Base64Benchmark.scala: -------------------------------------------------------------------------------- 1 | package base64 2 | 3 | import org.apache.commons.codec.binary.Base64 4 | import io.netty.handler.codec.base64.{ Base64 => NettyBase64 } 5 | import io.netty.buffer.{ ByteBuf, Unpooled } 6 | import com.google.caliper.SimpleBenchmark 7 | 8 | class Base64Benchmark extends SimpleBenchmark { 9 | def timeApacheEnc(n: Int) = for (i <- 0 to n) { 10 | Base64.encodeBase64(Bench.bytes) 11 | } 12 | 13 | def timeApacheDec(n: Int) = for (i <- 0 to n) { 14 | Base64.decodeBase64(Bench.encoded) 15 | } 16 | 17 | def timeNettyEnc(n: Int) = for (i <- 0 to n) { 18 | NettyBase64.encode(Unpooled.copiedBuffer(Bench.bytes)) 19 | } 20 | 21 | def timeNettyDec(n: Int) = for (i <- 0 to n) { 22 | NettyBase64.decode(Unpooled.copiedBuffer(Bench.encoded)) 23 | } 24 | 25 | def timeOurEnc(n: Int) = for (i <- 0 to n) { 26 | Encode(Bench.bytes) 27 | } 28 | 29 | def timeOurDecode(n: Int) = for (i <- 0 to n) { 30 | Decode(Bench.encoded) 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /src/test/scala/base64/Base64Spec.scala: -------------------------------------------------------------------------------- 1 | package base64 2 | 3 | import org.scalatest.FunSpec 4 | 5 | // all expected output generated by apache commons codec 6 | class Base64Spec extends FunSpec { 7 | 8 | describe ("Encode") { 9 | 10 | it ("should allow url unsafe output") { 11 | assert(str(Encode("hello world?")) === "aGVsbG8gd29ybGQ/") 12 | } 13 | 14 | it ("should encode short strings") { 15 | (("f", "Zg==") :: ("fo", "Zm8=") :: ("foo", "Zm9v") :: Nil).foreach { 16 | case (in, out) => assert(str(Encode(in)) === out) 17 | } 18 | } 19 | 20 | it ("should encode with and without padding") { 21 | val str = "easure." 22 | def check(pad: Boolean, expect: Array[Byte]) { 23 | val enc = Encode(str, pad = pad) 24 | assert(enc === expect) 25 | assert(Decode(enc).right.map(new String(_)) === Right(str)) 26 | } 27 | check(true, "ZWFzdXJlLg==".getBytes) 28 | check(false, "ZWFzdXJlLg".getBytes) 29 | } 30 | 31 | } 32 | 33 | describe ("Encode.urlSafe") { 34 | 35 | it ("should escape url unsafe output") { 36 | assert(str(Encode.urlSafe("hello world?")) === "aGVsbG8gd29ybGQ_") 37 | } 38 | 39 | } 40 | 41 | describe ("Decode") { 42 | 43 | it ("should not bother decoding pad only input") { 44 | ("====" :: "===" :: "==" :: "=" :: Nil).foreach { 45 | p => assert(Decode(p).right.map(_.size) === Right(0)) 46 | } 47 | } 48 | 49 | it ("should decode url unsafe strs") { 50 | assert(Decode("aGVsbG8gd29ybGQ/").right.map(str) === Right("hello world?")) 51 | } 52 | 53 | } 54 | 55 | describe ("Decode.urlSafe") { 56 | it ("should decode urlsafe strs") { 57 | assert(Decode.urlSafe("aGVsbG8gd29ybGQ_").right.map(str) === Right("hello world?")) 58 | } 59 | } 60 | 61 | def str(bytes: Array[Byte]) = new String(bytes) 62 | } 63 | -------------------------------------------------------------------------------- /src/test/scala/base64/bench.scala: -------------------------------------------------------------------------------- 1 | package base64 2 | 3 | import org.apache.commons.codec.binary.Base64 4 | import io.netty.handler.codec.base64.{ Base64 => NettyBase64 } 5 | import io.netty.buffer.{ ByteBuf, Unpooled } 6 | import java.nio.ByteBuffer 7 | 8 | object Bench { 9 | val bytes = "Man is distinguished, not only by his reason, but by this singular passion from other animals, which is a lust of the mind, that by a perseverance of delight in the continued and indefatigable generation of knowledge, exceeds the short vehemence of any carnal pleasure.".getBytes 10 | 11 | val encoded = "TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0aGlzIHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIGx1c3Qgb2YgdGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGludWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRoZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4=".getBytes 12 | 13 | def main(args: Array[String]) { 14 | 15 | def repeat(times: Int)(f: => Unit) = { 16 | val before = System.currentTimeMillis 17 | for (i <- 0 to times)(f) 18 | System.currentTimeMillis - before 19 | } 20 | 21 | def run(times: Int = 1000, log: Boolean = false) = { 22 | val apache = repeat(times) { Base64.encodeBase64(bytes) } 23 | val netty = repeat(times) { 24 | NettyBase64.encode(Unpooled.copiedBuffer(bytes)) 25 | } 26 | val ours = repeat(times) { 27 | Encode(bytes) 28 | } 29 | 30 | val apacheDec = repeat(times) { Base64.decodeBase64(encoded) } 31 | val nettyDec = repeat(times) { 32 | NettyBase64.decode(Unpooled.copiedBuffer(encoded)) 33 | } 34 | val oursDec = repeat(times) { 35 | Decode(encoded) 36 | } 37 | 38 | if (log) { 39 | println("enc apache commons (byte arrays) took %s ms" format apache) // 97ms / 15000 40 | println("enc netty (byte buf) took %s ms" format netty) // 95ms / 15000 41 | println("enc ours (byte arrays) took %s ms" format ours) // 121ms / 15000 42 | 43 | println("dec apache commons (byte arrays) took %s ms" format apacheDec) // 71ms / 15000 44 | println("dec netty (byte buf) took %s ms" format nettyDec) // 171ms / 15000 45 | println("dec ours (byte arrays) took %s ms" format oursDec) // 85ms / 15000 46 | } 47 | } 48 | 49 | // warmup 50 | run() 51 | 52 | // bench 53 | run(times = 15000, log = true) 54 | } 55 | } 56 | 57 | --------------------------------------------------------------------------------