Skip to content
This repository was archived by the owner on Jul 26, 2025. It is now read-only.

Commit bb5792f

Browse files
committed
add tests
1 parent 004b2a4 commit bb5792f

File tree

8 files changed

+154
-56
lines changed

8 files changed

+154
-56
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![standard-readme compliant](https://img.shields.io/badge/standard--readme-OK-green.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme)
44

5-
> Scala Implementation of Multibase.
5+
> Scala Implementation of [Multibase](https://github.com/multiformats/multibase).
66
77
TODO: Fill out this long description.
88

src/main/scala/com/github/fluency03/multibase/Base.scala

Lines changed: 62 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package com.github.fluency03.multibase
22

3+
34
sealed abstract class Base(
45
val name: String,
56
val code: Char,
6-
val alphabet: String,
7-
val pad: Option[Char]) {
7+
val alphabet: String) {
88

99
lazy val alphabetPos: Map[Char, Int] = (for (i <- alphabet.indices) yield alphabet(i) -> i).toMap
1010

@@ -14,49 +14,82 @@ sealed class Base16RFC4648(
1414
override val name: String,
1515
override val code: Char,
1616
override val alphabet: String,
17-
override val pad: Option[Char])
18-
extends Base(name, code, alphabet, pad)
17+
val pad: Option[Char])
18+
extends Base(name, code, alphabet)
1919

2020
sealed class Base32RFC4648(
2121
override val name: String,
2222
override val code: Char,
2323
override val alphabet: String,
24-
override val pad: Option[Char])
25-
extends Base(name, code, alphabet, pad)
24+
val pad: Option[Char])
25+
extends Base(name, code, alphabet)
2626

2727
sealed class Base64RFC4648(
2828
override val name: String,
2929
override val code: Char,
3030
override val alphabet: String,
31-
override val pad: Option[Char])
32-
extends Base(name, code, alphabet, pad)
31+
val pad: Option[Char])
32+
extends Base(name, code, alphabet)
3333

3434

3535
object Base {
36-
case object Identity extends Base("identity", 0x00, "", None)
37-
case object Base1 extends Base("base1", '1', "1", None)
38-
case object Base2 extends Base("base2", '0', "01", None)
39-
case object Base8 extends Base("base8", '7', "01234567", None)
40-
case object Base10 extends Base("base10", '9', "0123456789", None)
36+
37+
/**
38+
* Reference: https://github.com/multiformats/multibase/blob/master/multibase.csv
39+
* encoding codes name
40+
*
41+
* identity 0x00 8-bit binary (encoder and decoder keeps data unmodified)
42+
* base1 1 unary tends to be 11111
43+
* base2 0 binary has 1 and 0
44+
* base8 7 highest char in octal
45+
* base10 9 highest char in decimal
46+
* base16 F, f highest char in hex
47+
* base32 B, b rfc4648 - no padding - highest letter
48+
* base32pad C, c rfc4648 - with padding
49+
* base32hex V, v rfc4648 - no padding - highest char
50+
* base32hexpad T, t rfc4648 - with padding
51+
* base32z h z-base-32 - used by Tahoe-LAFS - highest letter
52+
* base58flickr Z highest char
53+
* base58btc z highest char
54+
* base64 m rfc4648 - no padding
55+
* base64pad M rfc4648 - with padding - MIME encoding
56+
* base64url u rfc4648 - no padding
57+
* base64urlpad U rfc4648 - with padding
58+
*/
59+
60+
case object Identity extends Base("identity", 0x00, "")
61+
case object Base1 extends Base("base1", '1', "1")
62+
case object Base2 extends Base("base2", '0', "01")
63+
case object Base8 extends Base("base8", '7', "01234567")
64+
case object Base10 extends Base("base10", '9', "0123456789")
65+
4166
case object Base16 extends Base16RFC4648("base16", 'f', "0123456789abcdef", None)
4267
case object Base16Upper extends Base16RFC4648("base16upper", 'F', "0123456789ABCDEF", None)
68+
4369
case object Base32 extends Base32RFC4648("base32", 'b', "abcdefghijklmnopqrstuvwxyz234567", None)
4470
case object Base32Upper extends Base32RFC4648("base32upper", 'B', "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567", None)
4571
case object Base32Pad extends Base32RFC4648("base32pad", 'c', "abcdefghijklmnopqrstuvwxyz234567", Some('='))
4672
case object Base32PadUpper extends Base32RFC4648("base32padupper", 'C', "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567", Some('='))
73+
4774
case object Base32Hex extends Base32RFC4648("base32hex", 'v', "0123456789abcdefghijklmnopqrstuv", None)
4875
case object Base32HexUpper extends Base32RFC4648("base32hexupper", 'V', "0123456789ABCDEFGHIJKLMNOPQRSTUV", None)
4976
case object Base32HexPad extends Base32RFC4648("base32hexpad", 't', "0123456789abcdefghijklmnopqrstuv", Some('='))
5077
case object Base32HexPadUpper extends Base32RFC4648("base32hexpadupper", 'T', "0123456789ABCDEFGHIJKLMNOPQRSTUV", Some('='))
51-
case object Base32Z extends Base("base32z", 'h', "ybndrfg8ejkmcpqxot1uwisza345h769", None)
52-
case object Base58Flickr extends Base("base58flickr", 'Z', "123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ", None)
53-
case object Base58BTC extends Base("base58btc", 'z', "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz", None)
78+
79+
case object Base32Z extends Base("base32z", 'h', "ybndrfg8ejkmcpqxot1uwisza345h769")
80+
case object Base58Flickr extends Base("base58flickr", 'Z', "123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ")
81+
case object Base58BTC extends Base("base58btc", 'z', "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz")
82+
5483
case object Base64 extends Base64RFC4648("base64", 'm', "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", None)
5584
case object Base64Pad extends Base64RFC4648("base64pad", 'M', "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", Some('='))
5685
case object Base64URL extends Base64RFC4648("base64url", 'u', "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_", None)
5786
case object Base64URLPad extends Base64RFC4648("base64urlpad", 'U', "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_", Some('='))
5887

59-
lazy val codes: Map[Char, Base] = Map(
88+
89+
/**
90+
* Mappings from Base Code -> Base
91+
*/
92+
lazy val Codes: Map[Char, Base] = Map(
6093
Identity.code -> Identity,
6194
Base1.code -> Base1,
6295
Base2.code -> Base2,
@@ -78,10 +111,13 @@ object Base {
78111
Base64.code -> Base64,
79112
Base64Pad.code -> Base64Pad,
80113
Base64URL.code -> Base64URL,
81-
Base64URLPad.code -> Base64URLPad
82-
)
114+
Base64URLPad.code -> Base64URLPad)
83115

84-
lazy val names: Map[String, Base] = Map(
116+
117+
/**
118+
* Mappings from Base Name -> Base
119+
*/
120+
lazy val Names: Map[String, Base] = Map(
85121
Identity.name -> Identity,
86122
Base1.name -> Base1,
87123
Base2.name -> Base2,
@@ -103,9 +139,12 @@ object Base {
103139
Base64.name -> Base64,
104140
Base64Pad.name -> Base64Pad,
105141
Base64URL.name -> Base64URL,
106-
Base64URLPad.name -> Base64URLPad
107-
)
142+
Base64URLPad.name -> Base64URLPad)
143+
108144

109-
lazy val unsupported: Map[Char, Base] = Map(Base1.code -> Base1)
145+
/**
146+
* Unsupported Base.
147+
*/
148+
lazy val Unsupported: Map[Char, Base] = Map(Base1.code -> Base1)
110149

111150
}

src/main/scala/com/github/fluency03/multibase/Base16Impl.scala

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,22 @@ package com.github.fluency03.multibase
33
object Base16Impl {
44

55
def encode(data: Array[Byte], base16: Base16RFC4648): String =
6-
data.flatMap(b => byteToChars(b, base16.alphabet.toCharArray)).mkString
6+
data.flatMap(b => byteToChars(b, base16.alphabet.toCharArray))
7+
.mkString
78

8-
def byteToChars(byte: Byte, alphabet: Array[Char]): Array[Char] = Array(
9+
private def byteToChars(byte: Byte, alphabet: Array[Char]): Array[Char] = Array(
910
alphabet(MASK_4BITS(byte >>> 4)),
1011
alphabet(MASK_4BITS(byte))
1112
)
1213

14+
1315
def decode(data: String, base16: Base16RFC4648): Array[Byte] =
14-
data.toCharArray.grouped(2).map(g => twoCharsToByte(g, base16.alphabetPos)).toArray
16+
data.toCharArray
17+
.grouped(2)
18+
.map(g => twoCharsToByte(g, base16.alphabetPos))
19+
.toArray
1520

16-
def twoCharsToByte(tuple: Array[Char], pos: Map[Char, Int]): Byte =
21+
private def twoCharsToByte(tuple: Array[Char], pos: Map[Char, Int]): Byte =
1722
(pos(tuple(0)) << 4 | pos(tuple(1))).toByte
1823

1924
}

src/main/scala/com/github/fluency03/multibase/Base32Impl.scala

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@ import scala.annotation.tailrec
44

55
object Base32Impl {
66
// operator precedence in descending order: >>> or <<, &, |
7-
private val BitGroupSize: Int = 5
8-
private val CommonGroupSize: Int = 8
7+
private val NumGroupsBeforeEncodeInLeastCommonLength: Int = 5
8+
private val NumGroupsAfterEncodeInLeastCommonLength: Int = 8
99

10+
/**
11+
* Encode the given data as an Array of Bytes into a String, based on specified Base32 version,
12+
* which has to be a Base32RFC4648.
13+
*/
1014
def encode(data: Array[Byte], base32: Base32RFC4648): String =
1115
if (data.isEmpty) ""
12-
else data.grouped(BitGroupSize)
16+
else data
17+
.grouped(NumGroupsBeforeEncodeInLeastCommonLength)
1318
.map(g => encodeBytes(g, base32.alphabet.toCharArray, base32.pad))
1419
.mkString
1520

@@ -69,20 +74,24 @@ object Base32Impl {
6974
).mkString
7075

7176

72-
77+
/**
78+
* Decode the given data as a String into a Byte Array, based on specified Base32 version,
79+
* which has to be a Base32RFC4648.
80+
*/
7381
def decode(data: String, base32: Base32RFC4648): Array[Byte] = {
7482
val chars = data.toCharArray
7583
val length = chars.length
7684
val pads = if (base32.pad.isEmpty) 0 else numPads(chars, 0, length - 1, base32.pad.get)
7785
val pos = base32.alphabetPos
78-
chars.slice(0, length - pads).grouped(CommonGroupSize)
86+
chars.slice(0, length - pads)
87+
.grouped(NumGroupsAfterEncodeInLeastCommonLength)
7988
.map(g => decodeBytes(g, pos))
8089
.foldLeft(Array[Byte]())( _ ++ _ )
8190
}
8291

8392
@tailrec
8493
private def numPads(chars: Array[Char], pads: Int, last: Int, pad: Char): Int =
85-
if (last < 0 || pads >= BitGroupSize || chars(last) != pad) pads
94+
if (last < 0 || pads >= NumGroupsBeforeEncodeInLeastCommonLength || chars(last) != pad) pads
8695
else numPads(chars, pads + 1, last - 1, pad)
8796

8897
private def decodeBytes(group: Array[Char], pos: Map[Char, Int]): Array[Byte] =

src/main/scala/com/github/fluency03/multibase/Base64Impl.scala

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@ import scala.annotation.tailrec
44

55
object Base64Impl {
66
// operator precedence in descending order: >>> or <<, &, |
7-
private val BitGroupSize: Int = 3
8-
private val CommonGroupSize: Int = 4
7+
private val NumGroupsBeforeEncodeInLeastCommonLength: Int = 3
8+
private val NumGroupsAfterEncodeInLeastCommonLength: Int = 4
99

10+
/**
11+
* Encode the given data as an Array of Bytes into a String, based on specified Base64 version,
12+
* which has to be a Base64RFC4648.
13+
*/
1014
def encode(data: Array[Byte], base64: Base64RFC4648): String =
1115
if (data.isEmpty) ""
12-
else data.grouped(BitGroupSize)
16+
else data
17+
.grouped(NumGroupsBeforeEncodeInLeastCommonLength)
1318
.map(g => encodeBytes(g, base64.alphabet.toCharArray, base64.pad))
1419
.mkString
1520

@@ -42,19 +47,24 @@ object Base64Impl {
4247
).mkString
4348

4449

50+
/**
51+
* Decode the given data as a String into a Byte Array, based on specified Base64 version,
52+
* which has to be a Base64RFC4648.
53+
*/
4554
def decode(data: String, base64: Base64RFC4648): Array[Byte] = {
4655
val chars = data.toCharArray
4756
val length = chars.length
4857
val pads = if (base64.pad.isEmpty) 0 else numPads(chars, 0, length - 1, base64.pad.get)
4958
val pos = base64.alphabetPos
50-
chars.slice(0, length - pads).grouped(CommonGroupSize)
59+
chars.slice(0, length - pads)
60+
.grouped(NumGroupsAfterEncodeInLeastCommonLength)
5161
.map(g => decodeBytes(g, pos))
5262
.foldLeft(Array[Byte]())( _ ++ _ )
5363
}
5464

5565
@tailrec
5666
private def numPads(chars: Array[Char], pads: Int, last: Int, pad: Char): Int =
57-
if (last < 0 || pads >= BitGroupSize || chars(last) != pad) pads
67+
if (last < 0 || pads >= NumGroupsBeforeEncodeInLeastCommonLength || chars(last) != pad) pads
5868
else numPads(chars, pads + 1, last - 1, pad)
5969

6070
private def decodeBytes(group: Array[Char], pos: Map[Char, Int]): Array[Byte] =
@@ -66,18 +76,18 @@ object Base64Impl {
6676
}
6777

6878
private def decode4Bytes(group: Array[Char], pos: Map[Char, Int]): Array[Byte] = Array(
69-
(pos(group(0)) << 2 | MASK_2BITS(pos(group(1)) >>> 4)).toByte, // 6 2
70-
(MASK_4BITS(pos(group(1))) << 4 | MASK_4BITS(pos(group(2)) >>> 2)).toByte, // 4 4
71-
(MASK_2BITS(pos(group(2))) << 6 | pos(group(3))).toByte // 2 6
79+
(pos(group(0)) << 2 | MASK_2BITS(pos(group(1)) >>> 4)).toByte, // 6 2
80+
(MASK_4BITS(pos(group(1))) << 4 | MASK_4BITS(pos(group(2)) >>> 2)).toByte, // 4 4
81+
(MASK_2BITS(pos(group(2))) << 6 | pos(group(3))).toByte // 2 6
7282
)
7383

7484
private def decode3Bytes(group: Array[Char], pos: Map[Char, Int]): Array[Byte] = Array(
75-
(pos(group(0)) << 2 | MASK_2BITS(pos(group(1)) >>> 4)).toByte, // 6 2
76-
(MASK_4BITS(pos(group(1))) << 4 | MASK_4BITS(pos(group(2)) >>> 2)).toByte // 4 4
85+
(pos(group(0)) << 2 | MASK_2BITS(pos(group(1)) >>> 4)).toByte, // 6 2
86+
(MASK_4BITS(pos(group(1))) << 4 | MASK_4BITS(pos(group(2)) >>> 2)).toByte // 4 4
7787
)
7888

7989
private def decode2Bytes(group: Array[Char], pos: Map[Char, Int]): Array[Byte] = Array(
80-
(pos(group(0)) << 2 | MASK_2BITS(pos(group(1)) >>> 4)).toByte // 6 2
90+
(pos(group(0)) << 2 | MASK_2BITS(pos(group(1)) >>> 4)).toByte // 6 2
8191
)
8292

8393
private def decode1Byte(group: Array[Char], pos: Map[Char, Int]): Array[Byte] = Array()

src/main/scala/com/github/fluency03/multibase/IdentityImpl.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package com.github.fluency03.multibase
22

33
object IdentityImpl {
44

5-
def encode(data: Array[Byte]): String = data.map(_.toChar).mkString
5+
def encode(data: Array[Byte]): String = new String(data)
66

77
def decode(data: String): Array[Byte] = data.getBytes()
88

src/main/scala/com/github/fluency03/multibase/Multibase.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ object Multibase {
2929
def encodeString(base: Base, data: String): String = encode(base, data.getBytes)
3030

3131
def decode(data: String): Array[Byte] = {
32-
val baseOpt = Base.codes.get(data.charAt(0))
32+
val baseOpt = Base.Codes.get(data.charAt(0))
3333
if (baseOpt.isEmpty) {
3434
throw new IllegalArgumentException("Cannot get Multibase type from input data: " + data)
3535
}

0 commit comments

Comments
 (0)