├── .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 | [](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 |
--------------------------------------------------------------------------------