Skip to content

Commit 422a86f

Browse files
committed
odometer
1 parent 337419c commit 422a86f

11 files changed

+668
-3
lines changed

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
default: test
44

55
bench:
6-
go test -bench=. -benchmem ./passphrase ./password ./password/sequencer
6+
go test -bench=. -benchmem ./odometer ./passphrase ./password ./password/sequencer
77

88
cyclo:
99
gocyclo -over 13 ./*/*.go

odometer/errors.go

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package odometer
2+
3+
import "errors"
4+
5+
var (
6+
ErrInvalidLocation = errors.New("invalid location")
7+
)

odometer/interface.go

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package odometer
2+
3+
import (
4+
"math/big"
5+
)
6+
7+
type Odometer interface {
8+
Decrement()
9+
DecrementN(n *big.Int)
10+
First()
11+
GetLocation() *big.Int
12+
Increment()
13+
IncrementN(n *big.Int)
14+
Last()
15+
SetLocation(n *big.Int) error
16+
String() string
17+
Value() []int
18+
}

odometer/odometer.go

+235
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
package odometer
2+
3+
import (
4+
"math/big"
5+
"slices"
6+
"sync"
7+
8+
"github.com/jedib0t/go-passwords/charset"
9+
)
10+
11+
var (
12+
biZero = big.NewInt(0)
13+
biOne = big.NewInt(1)
14+
)
15+
16+
type odometer struct {
17+
base int
18+
baseBigInt *big.Int
19+
charset []rune
20+
length int
21+
location *big.Int
22+
locationMax *big.Int
23+
mutex sync.RWMutex
24+
rollover bool
25+
value []int
26+
valueInCharset []rune
27+
}
28+
29+
func New(cs charset.Charset, length int, opts ...Option) Odometer {
30+
base := len(cs)
31+
maxValues := numValues(base, length)
32+
33+
o := &odometer{
34+
base: base,
35+
baseBigInt: big.NewInt(int64(base)),
36+
charset: []rune(cs),
37+
length: length,
38+
location: big.NewInt(1),
39+
locationMax: new(big.Int).Set(maxValues),
40+
value: make([]int, length),
41+
valueInCharset: make([]rune, length),
42+
}
43+
for _, opt := range opts {
44+
opt(o)
45+
}
46+
return o
47+
}
48+
49+
func (o *odometer) Decrement() {
50+
o.mutex.Lock()
51+
defer o.mutex.Unlock()
52+
53+
// set the location
54+
if o.location.Cmp(biOne) == 0 { // at first
55+
if o.rollover {
56+
o.last()
57+
}
58+
return
59+
}
60+
61+
// decrement value
62+
o.location.Sub(o.location, biOne)
63+
for idx := o.length - 1; idx >= 0; idx-- {
64+
if o.decrementAtIndex(idx) {
65+
return
66+
}
67+
}
68+
}
69+
70+
func (o *odometer) DecrementN(n *big.Int) {
71+
o.mutex.Lock()
72+
defer o.mutex.Unlock()
73+
74+
o.location.Sub(o.location, n)
75+
if o.location.Cmp(biOne) < 0 { // less than min
76+
if !o.rollover {
77+
o.first()
78+
return
79+
}
80+
// move backwards from max; o.location is currently -ve --> so Add()
81+
for o.location.Cmp(biOne) < 0 {
82+
o.location.Add(o.locationMax, o.location)
83+
}
84+
}
85+
o.computeValue()
86+
}
87+
88+
func (o *odometer) First() {
89+
o.mutex.Lock()
90+
defer o.mutex.Unlock()
91+
92+
o.first()
93+
}
94+
95+
func (o *odometer) GetLocation() *big.Int {
96+
o.mutex.RLock()
97+
defer o.mutex.RUnlock()
98+
99+
return new(big.Int).Set(o.location)
100+
}
101+
102+
func (o *odometer) Increment() {
103+
o.mutex.Lock()
104+
defer o.mutex.Unlock()
105+
106+
// set the location
107+
if o.location.Cmp(o.locationMax) == 0 { // at max
108+
if o.rollover {
109+
o.first()
110+
}
111+
return
112+
}
113+
114+
// increment value
115+
o.location.Add(o.location, biOne)
116+
for idx := o.length - 1; idx >= 0; idx-- {
117+
if o.incrementAtIndex(idx) {
118+
return
119+
}
120+
}
121+
}
122+
123+
func (o *odometer) IncrementN(n *big.Int) {
124+
o.mutex.Lock()
125+
defer o.mutex.Unlock()
126+
127+
o.location.Add(o.location, n)
128+
if o.location.Cmp(o.locationMax) > 0 { // more than max
129+
if !o.rollover {
130+
o.last()
131+
return
132+
}
133+
// move forwards from zero
134+
for o.location.Cmp(o.locationMax) > 0 {
135+
o.location.Sub(o.location, o.locationMax)
136+
}
137+
}
138+
o.computeValue()
139+
}
140+
141+
func (o *odometer) Last() {
142+
o.mutex.Lock()
143+
defer o.mutex.Unlock()
144+
145+
o.last()
146+
}
147+
148+
func (o *odometer) SetLocation(n *big.Int) error {
149+
o.mutex.Lock()
150+
defer o.mutex.Unlock()
151+
152+
if n.Cmp(biOne) < 0 || n.Cmp(o.locationMax) > 0 {
153+
return ErrInvalidLocation
154+
}
155+
o.location.Set(n)
156+
o.computeValue()
157+
return nil
158+
}
159+
160+
func (o *odometer) String() string {
161+
o.mutex.Lock()
162+
defer o.mutex.Unlock()
163+
164+
for idx := range o.valueInCharset {
165+
o.valueInCharset[idx] = o.charset[o.value[idx]]
166+
}
167+
return string(o.valueInCharset)
168+
}
169+
170+
func (o *odometer) Value() []int {
171+
o.mutex.Lock()
172+
defer o.mutex.Unlock()
173+
174+
return slices.Clone(o.value)
175+
}
176+
177+
func (o *odometer) computeValue() {
178+
// base conversion: convert the value of location to a decimal with the
179+
// given base using continuous division and use all the remainders as the
180+
// values
181+
182+
// prep the dividend, remainder and modulus
183+
dividend, remainder := new(big.Int).Sub(o.location, biOne), new(big.Int)
184+
// append values in reverse (from right to left)
185+
valIdx := o.length - 1
186+
// append every remainder until dividend becomes zero
187+
for ; dividend.Cmp(biZero) != 0; valIdx-- {
188+
dividend, remainder = dividend.QuoRem(dividend, o.baseBigInt, remainder)
189+
o.value[valIdx] = int(remainder.Int64())
190+
}
191+
// left-pad the remaining characters with 0 (==> 0th char in charset)
192+
for ; valIdx >= 0; valIdx-- {
193+
o.value[valIdx] = 0
194+
}
195+
}
196+
197+
func (o *odometer) decrementAtIndex(idx int) bool {
198+
if o.value[idx] > 0 {
199+
o.value[idx]--
200+
return true
201+
}
202+
if o.value[idx] == 0 && idx > 0 {
203+
o.value[idx] = o.base - 1
204+
o.decrementAtIndex(idx - 1)
205+
return true
206+
}
207+
return false
208+
}
209+
210+
func (o *odometer) first() {
211+
o.location.Set(biOne)
212+
for idx := range o.value {
213+
o.value[idx] = 0
214+
}
215+
}
216+
217+
func (o *odometer) incrementAtIndex(idx int) bool {
218+
if o.value[idx] < o.base-1 {
219+
o.value[idx]++
220+
return true
221+
}
222+
if o.value[idx] == o.base-1 && idx > 0 {
223+
o.value[idx] = 0
224+
o.incrementAtIndex(idx - 1)
225+
return true
226+
}
227+
return false
228+
}
229+
230+
func (o *odometer) last() {
231+
o.location.Set(o.locationMax)
232+
for idx := range o.value {
233+
o.value[idx] = o.base - 1
234+
}
235+
}

odometer/odometer_bench_test.go

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package odometer
2+
3+
import (
4+
"math"
5+
"math/big"
6+
"math/rand"
7+
"testing"
8+
"time"
9+
10+
"github.com/jedib0t/go-passwords/charset"
11+
"github.com/stretchr/testify/assert"
12+
)
13+
14+
func BenchmarkOdometer_Big_Decrement(b *testing.B) {
15+
o := New(charset.AllChars, 256)
16+
o.Last()
17+
18+
for i := 0; i < b.N; i++ {
19+
o.Decrement()
20+
}
21+
}
22+
23+
func BenchmarkOdometer_Big_Increment(b *testing.B) {
24+
o := New(charset.AllChars, 256)
25+
26+
for i := 0; i < b.N; i++ {
27+
o.Increment()
28+
}
29+
}
30+
31+
func BenchmarkOdometer_Decrement(b *testing.B) {
32+
o := New(charset.Numbers, 8, WithRolloverEnabled(true))
33+
34+
for i := 0; i < b.N; i++ {
35+
o.Decrement()
36+
}
37+
}
38+
39+
func BenchmarkOdometer_DecrementN(b *testing.B) {
40+
o := New(charset.Numbers, 8, WithRolloverEnabled(true))
41+
42+
n := big.NewInt(5)
43+
for i := 0; i < b.N; i++ {
44+
o.DecrementN(n)
45+
}
46+
}
47+
48+
func BenchmarkOdometer_Increment(b *testing.B) {
49+
o := New(charset.Numbers, 8, WithRolloverEnabled(true))
50+
51+
for i := 0; i < b.N; i++ {
52+
o.Increment()
53+
}
54+
}
55+
56+
func BenchmarkOdometer_IncrementN(b *testing.B) {
57+
o := New(charset.Numbers, 8, WithRolloverEnabled(true))
58+
59+
n := big.NewInt(5)
60+
for i := 0; i < b.N; i++ {
61+
o.IncrementN(n)
62+
}
63+
}
64+
65+
func BenchmarkOdometer_SetLocation(b *testing.B) {
66+
o := New(charset.Numbers, 8, WithRolloverEnabled(true))
67+
maxValues := int64(math.Pow(10, 8))
68+
rng := rand.New(rand.NewSource(time.Now().Unix()))
69+
70+
for i := 0; i < b.N; i++ {
71+
n := big.NewInt(rng.Int63n(maxValues))
72+
err := o.SetLocation(n)
73+
assert.Nil(b, err)
74+
}
75+
}
76+
77+
func BenchmarkOdometer_String(b *testing.B) {
78+
o := New(charset.Numbers, 12)
79+
80+
for i := 0; i < b.N; i++ {
81+
_ = o.String()
82+
}
83+
}

0 commit comments

Comments
 (0)