Skip to content

Commit d1fa692

Browse files
committed
PR2 Deprecate Hashids.apply. Implement Hashids.legacyJiecao and Hashids.reference.
1 parent 2db5a1a commit d1fa692

File tree

5 files changed

+155
-83
lines changed

5 files changed

+155
-83
lines changed

README.md

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,20 @@ If you're using SBT, add the following lines to your build file:
3232

3333
libraryDependencies += "io.john-ky" %% "hashids-scala" % "1.1.0-448df98"
3434

35+
## Compatibility note
36+
37+
In version `1.1.0`, the library contained a bug that caused it to generate hashids that were incompatibility with the
38+
Javascript reference implementation under some situations. Applications upgrading from `1.1.0` to `1.1.1` will need to
39+
decide which of cross-language compatibility or backwards compatibility is more important.
40+
41+
To ensure that the application writers make this decision carefully, the `Hashids.apply` method is deprecated and will
42+
now throw `NotImplementedException`.
43+
44+
Application writers are must replace calls to `Hashids.apply` with one of the following:
45+
46+
* `Hashids.legacyJiecao` - if backwards compatibility is important.
47+
* `Hashids.reference` - if cross-language compatibility is important.
48+
3549
## Usage
3650

3751
#### Import the package
@@ -46,7 +60,7 @@ You can pass a unique salt value so your hashes differ from everyone else's. "t
4660

4761
```scala
4862

49-
val hashids = Hashids("this is my salt")
63+
val hashids = Hashids.reference("this is my salt")
5064
val hash = hashids.encode(12345L)
5165
```
5266

@@ -60,7 +74,7 @@ Notice during decryption, same salt value is used:
6074

6175
```scala
6276

63-
val hashids = Hashids("this is my salt")
77+
val hashids = Hashids.reference("this is my salt")
6478
val numbers = hashids.decode("NkK9")
6579
```
6680

@@ -74,7 +88,7 @@ Decryption will not work if salt is changed:
7488

7589
```scala
7690

77-
val hashids = Hashids("this is my pepper")
91+
val hashids = Hashids.reference("this is my pepper")
7892
val numbers = hashids.decode("NkK9")
7993
```
8094

@@ -86,7 +100,7 @@ val numbers = hashids.decode("NkK9")
86100

87101
```scala
88102

89-
val hashids = Hashids("this is my salt")
103+
val hashids = Hashids.reference("this is my salt")
90104
val hash = hashids.encode(683L, 94108L, 123L, 5L)
91105
```
92106

@@ -98,7 +112,7 @@ val hash = hashids.encode(683L, 94108L, 123L, 5L)
98112

99113
```scala
100114

101-
val hashids = Hashids("this is my salt")
115+
val hashids = Hashids.reference("this is my salt")
102116
val numbers = hashids.decode("aBMswoO2UB3Sj")
103117
```
104118

@@ -112,7 +126,7 @@ Here we encrypt integer 1, and set the minimum hash length to **8** (by default
112126

113127
```scala
114128

115-
val hashids = Hashids("this is my salt", 8)
129+
val hashids = Hashids.reference("this is my salt", 8)
116130
val hash = hashids.encode(1L)
117131
```
118132

@@ -124,7 +138,7 @@ val hash = hashids.encode(1L)
124138

125139
```scala
126140

127-
val hashids = Hashids("this is my salt", 8)
141+
val hashids = Hashids.reference("this is my salt", 8)
128142
val numbers = hashids.decode("gB0NV05e")
129143
```
130144

@@ -138,7 +152,7 @@ Here we set the alphabet to consist of only four letters: "0123456789abcdef"
138152

139153
```scala
140154

141-
val hashids = Hashids("this is my salt", 0, "0123456789abcdef")
155+
val hashids = Hashids.reference("this is my salt", 0, "0123456789abcdef")
142156
val hash = hashids.encode(1234567L)
143157
```
144158

@@ -155,7 +169,7 @@ Having said that, this algorithm does try to make these hashes unguessable and u
155169

156170
```scala
157171

158-
val hashids = Hashids("this is my salt")
172+
val hashids = Hashids.reference("this is my salt")
159173
val hash = hashids.encode(5L, 5L, 5L, 5L)
160174
```
161175

@@ -167,7 +181,7 @@ Same with incremented numbers:
167181

168182
```scala
169183

170-
val hashids = Hashids("this is my salt")
184+
val hashids = Hashids.reference("this is my salt")
171185
val hash = hashids.encode(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L)
172186
```
173187

@@ -179,7 +193,7 @@ val hash = hashids.encode(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L)
179193

180194
```scala
181195

182-
val hashids = Hashids("this is my salt")
196+
val hashids = Hashids.reference("this is my salt")
183197
val hash1 = hashids.encode(1L) /* NV */
184198
val hash2 = hashids.encode(2L) /* 6m */
185199
val hash3 = hashids.encode(3L) /* yD */
@@ -199,7 +213,7 @@ In the following examples, the `Long` and `Seq[Long]` is lifted to support the
199213
import org.hashids.Hashids
200214
import org.hashids.syntax._
201215

202-
implicit val hashids = Hashids("this is my salt")
216+
implicit val hashids = Hashids.reference("this is my salt")
203217
val hash1 = 12345L.hashid
204218
val hash2 = List(1L, 2L, 3L).hashid
205219
val unhashed = "NkK9".unhashid

hashids-scala/src/main/scala/org/hashids/Hashids.scala

Lines changed: 109 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -6,51 +6,10 @@ import org.hashids.impl._
66
class Hashids(
77
salt: String = "",
88
minHashLength: Int = 0,
9-
alphabet: String = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890") {
10-
private val (seps, guards, effectiveAlphabet) = {
11-
val distinctAlphabet = alphabet.distinct
12-
13-
require(distinctAlphabet.length >= 16, "alphabet must contain at least 16 unique characters")
14-
require(distinctAlphabet.indexOf(" ") < 0, "alphabet cannot contains spaces")
15-
16-
val sepDiv = 3.5
17-
val guardDiv = 12
18-
val filteredSeps = "cfhistuCFHISTU".filter(x => distinctAlphabet.contains(x))
19-
val filteredAlphabet = distinctAlphabet.filterNot(x => filteredSeps.contains(x))
20-
val shuffledSeps = consistentShuffle(filteredSeps, salt)
21-
22-
val (tmpSeps, tmpAlpha) = {
23-
if (shuffledSeps.isEmpty || ((filteredAlphabet.length / shuffledSeps.length) > sepDiv)) {
24-
val sepsTmpLen = Math.ceil(filteredAlphabet.length / sepDiv).toInt
25-
val sepsLen = if (sepsTmpLen == 1) 2 else sepsTmpLen
26-
27-
if (sepsLen > shuffledSeps.length) {
28-
val diff = sepsLen - shuffledSeps.length
29-
val seps = shuffledSeps + filteredAlphabet.substring(0, diff)
30-
val alpha = filteredAlphabet.substring(diff)
31-
(seps, alpha)
32-
} else {
33-
val seps = shuffledSeps.substring(0, sepsLen)
34-
val alpha = filteredAlphabet
35-
(seps, alpha)
36-
}
37-
} else (shuffledSeps, filteredAlphabet)
38-
}
39-
40-
val guardCount = Math.ceil(tmpAlpha.length.toDouble / guardDiv).toInt
41-
val shuffledAlpha = consistentShuffle(tmpAlpha, salt)
42-
43-
if (shuffledAlpha.length < 3) {
44-
val guards = tmpSeps.substring(0, guardCount)
45-
val seps = tmpSeps.substring(guardCount)
46-
(seps, guards, shuffledAlpha)
47-
} else {
48-
val guards = shuffledAlpha.substring(0, guardCount)
49-
val alpha = shuffledAlpha.substring(guardCount)
50-
(tmpSeps, guards, alpha)
51-
}
52-
}
53-
9+
alphabet: String = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890",
10+
seps: String,
11+
guards: String,
12+
effectiveAlphabet: String) {
5413
def encode(numbers: Long*): String = {
5514
if (numbers.isEmpty) {
5615
""
@@ -88,12 +47,111 @@ class Hashids(
8847
}
8948

9049
object Hashids {
91-
def apply(salt: String) =
92-
new Hashids(salt)
50+
@deprecated("Use `Hashids.legacyJiecao` or `Hashids.reference` instead. See compatibility note in README.md", "1.1.1")
51+
def apply(
52+
salt: String = "",
53+
minHashLength: Int = 0,
54+
alphabet: String = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"): Hashids = ???
55+
56+
def legacyJiecao(
57+
salt: String = "",
58+
minHashLength: Int = 0,
59+
alphabet: String = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890") = {
60+
val (seps, guards, effectiveAlphabet) = {
61+
val distinctAlphabet = alphabet.distinct
62+
63+
require(distinctAlphabet.length >= 16, "alphabet must contain at least 16 unique characters")
64+
require(distinctAlphabet.indexOf(" ") < 0, "alphabet cannot contains spaces")
65+
66+
val sepDiv = 3.5
67+
val guardDiv = 12
68+
val filteredSeps = "cfhistuCFHISTU".filter(x => distinctAlphabet.contains(x))
69+
val filteredAlphabet = distinctAlphabet.filterNot(x => filteredSeps.contains(x))
70+
val shuffledSeps = consistentShuffle(filteredSeps, salt)
71+
72+
val (tmpSeps, tmpAlpha) = {
73+
if (shuffledSeps.isEmpty || ((filteredAlphabet.length / shuffledSeps.length) > sepDiv)) {
74+
val sepsTmpLen = Math.ceil(filteredAlphabet.length / sepDiv).toInt
75+
val sepsLen = if (sepsTmpLen == 1) 2 else sepsTmpLen
76+
77+
if (sepsLen > shuffledSeps.length) {
78+
val diff = sepsLen - shuffledSeps.length
79+
val seps = shuffledSeps + filteredAlphabet.substring(0, diff)
80+
val alpha = filteredAlphabet.substring(diff)
81+
(seps, alpha)
82+
} else {
83+
val seps = shuffledSeps.substring(0, sepsLen)
84+
val alpha = filteredAlphabet
85+
(seps, alpha)
86+
}
87+
} else (shuffledSeps, filteredAlphabet)
88+
}
89+
90+
val guardCount = Math.ceil(tmpAlpha.length.toDouble / guardDiv).toInt
91+
val shuffledAlpha = consistentShuffle(tmpAlpha, salt)
92+
93+
if (shuffledAlpha.length < 3) {
94+
val guards = tmpSeps.substring(0, guardCount)
95+
val seps = tmpSeps.substring(guardCount)
96+
(seps, guards, shuffledAlpha)
97+
} else {
98+
val guards = shuffledAlpha.substring(0, guardCount)
99+
val alpha = shuffledAlpha.substring(guardCount)
100+
(tmpSeps, guards, alpha)
101+
}
102+
}
103+
104+
new Hashids(salt, minHashLength, alphabet, seps, guards, effectiveAlphabet)
105+
}
93106

94-
def apply(salt: String, minHashLength: Int) =
95-
new Hashids(salt, minHashLength)
107+
def reference(
108+
salt: String = "",
109+
minHashLength: Int = 0,
110+
alphabet: String = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890") = {
111+
val (seps, guards, effectiveAlphabet) = {
112+
val distinctAlphabet = alphabet.distinct
113+
114+
require(distinctAlphabet.length >= 16, "alphabet must contain at least 16 unique characters")
115+
require(distinctAlphabet.indexOf(" ") < 0, "alphabet cannot contains spaces")
116+
117+
val sepDiv = 3.5
118+
val guardDiv = 12
119+
val filteredSeps = "cfhistuCFHISTU".filter(x => distinctAlphabet.contains(x))
120+
val filteredAlphabet = distinctAlphabet.filterNot(x => filteredSeps.contains(x))
121+
val shuffledSeps = consistentShuffle(filteredSeps, salt)
122+
123+
val (tmpSeps, tmpAlpha) = {
124+
if (shuffledSeps.isEmpty || (Math.ceil(filteredAlphabet.length.toDouble / shuffledSeps.length) > sepDiv)) {
125+
val sepsTmpLen = Math.ceil(filteredAlphabet.length / sepDiv).toInt
126+
val sepsLen = if (sepsTmpLen == 1) 2 else sepsTmpLen
127+
128+
if (sepsLen > shuffledSeps.length) {
129+
val diff = sepsLen - shuffledSeps.length
130+
val seps = shuffledSeps + filteredAlphabet.substring(0, diff)
131+
val alpha = filteredAlphabet.substring(diff)
132+
(seps, alpha)
133+
} else {
134+
val seps = shuffledSeps.substring(0, sepsLen)
135+
val alpha = filteredAlphabet
136+
(seps, alpha)
137+
}
138+
} else (shuffledSeps, filteredAlphabet)
139+
}
140+
141+
val guardCount = Math.ceil(tmpAlpha.length.toDouble / guardDiv).toInt
142+
val shuffledAlpha = consistentShuffle(tmpAlpha, salt)
143+
144+
if (shuffledAlpha.length < 3) {
145+
val guards = tmpSeps.substring(0, guardCount)
146+
val seps = tmpSeps.substring(guardCount)
147+
(seps, guards, shuffledAlpha)
148+
} else {
149+
val guards = shuffledAlpha.substring(0, guardCount)
150+
val alpha = shuffledAlpha.substring(guardCount)
151+
(tmpSeps, guards, alpha)
152+
}
153+
}
96154

97-
def apply(salt: String, minHashLength: Int, alphabet: String) =
98-
new Hashids(salt, minHashLength, alphabet)
155+
new Hashids(salt, minHashLength, alphabet, seps, guards, effectiveAlphabet)
156+
}
99157
}

hashids-scala/src/test/scala/org/hashids/CheckHashids.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,23 @@ class CheckHashids extends Specification with ScalaCheck {
1414

1515
"List of random zero or positive longs should encode then decode" in {
1616
prop { (a: List[ZeroOrPosLong], salt: String) =>
17-
implicit val hashid = Hashids(salt)
17+
implicit val hashid = Hashids.reference(salt)
1818

1919
a.raw.hashid.unhashid ==== a.raw
2020
}
2121
}
2222

2323
"List of random zero or positive longs should encode then decode" in {
2424
prop { (a: List[ZeroOrPosLong], salt: String) =>
25-
implicit val hashid = Hashids(salt = salt)
25+
implicit val hashid = Hashids.reference(salt = salt)
2626

2727
a.raw.hashid.unhashid ==== a.raw
2828
}
2929
}
3030

3131
"List of random zero or positive longs should encode then decode and honour min hash length" in {
3232
prop { (a: List[ZeroOrPosLong], salt: String, minHashLength: Size) =>
33-
implicit val hashid = Hashids(salt = salt, minHashLength = minHashLength.size)
33+
implicit val hashid = Hashids.reference(salt = salt, minHashLength = minHashLength.size)
3434

3535
val hash = a.raw.hashid
3636

0 commit comments

Comments
 (0)