Skip to content

Commit

Permalink
odometer
Browse files Browse the repository at this point in the history
  • Loading branch information
jedib0t committed Apr 22, 2024
1 parent 337419c commit 422a86f
Show file tree
Hide file tree
Showing 11 changed files with 668 additions and 3 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
default: test

bench:
go test -bench=. -benchmem ./passphrase ./password ./password/sequencer
go test -bench=. -benchmem ./odometer ./passphrase ./password ./password/sequencer

cyclo:
gocyclo -over 13 ./*/*.go
Expand Down
7 changes: 7 additions & 0 deletions odometer/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package odometer

import "errors"

var (
ErrInvalidLocation = errors.New("invalid location")
)
18 changes: 18 additions & 0 deletions odometer/interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package odometer

import (
"math/big"
)

type Odometer interface {
Decrement()
DecrementN(n *big.Int)
First()
GetLocation() *big.Int
Increment()
IncrementN(n *big.Int)
Last()
SetLocation(n *big.Int) error
String() string
Value() []int
}
235 changes: 235 additions & 0 deletions odometer/odometer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
package odometer

import (
"math/big"
"slices"
"sync"

"github.com/jedib0t/go-passwords/charset"
)

var (
biZero = big.NewInt(0)
biOne = big.NewInt(1)
)

type odometer struct {
base int
baseBigInt *big.Int
charset []rune
length int
location *big.Int
locationMax *big.Int
mutex sync.RWMutex
rollover bool
value []int
valueInCharset []rune
}

func New(cs charset.Charset, length int, opts ...Option) Odometer {
base := len(cs)
maxValues := numValues(base, length)

o := &odometer{
base: base,
baseBigInt: big.NewInt(int64(base)),
charset: []rune(cs),
length: length,
location: big.NewInt(1),
locationMax: new(big.Int).Set(maxValues),
value: make([]int, length),
valueInCharset: make([]rune, length),
}
for _, opt := range opts {
opt(o)
}
return o
}

func (o *odometer) Decrement() {
o.mutex.Lock()
defer o.mutex.Unlock()

// set the location
if o.location.Cmp(biOne) == 0 { // at first
if o.rollover {
o.last()
}
return
}

// decrement value
o.location.Sub(o.location, biOne)
for idx := o.length - 1; idx >= 0; idx-- {
if o.decrementAtIndex(idx) {
return
}
}
}

func (o *odometer) DecrementN(n *big.Int) {
o.mutex.Lock()
defer o.mutex.Unlock()

o.location.Sub(o.location, n)
if o.location.Cmp(biOne) < 0 { // less than min
if !o.rollover {
o.first()
return
}
// move backwards from max; o.location is currently -ve --> so Add()
for o.location.Cmp(biOne) < 0 {
o.location.Add(o.locationMax, o.location)
}
}
o.computeValue()
}

func (o *odometer) First() {
o.mutex.Lock()
defer o.mutex.Unlock()

o.first()
}

func (o *odometer) GetLocation() *big.Int {
o.mutex.RLock()
defer o.mutex.RUnlock()

return new(big.Int).Set(o.location)
}

func (o *odometer) Increment() {
o.mutex.Lock()
defer o.mutex.Unlock()

// set the location
if o.location.Cmp(o.locationMax) == 0 { // at max
if o.rollover {
o.first()
}
return
}

// increment value
o.location.Add(o.location, biOne)
for idx := o.length - 1; idx >= 0; idx-- {
if o.incrementAtIndex(idx) {
return
}
}
}

func (o *odometer) IncrementN(n *big.Int) {
o.mutex.Lock()
defer o.mutex.Unlock()

o.location.Add(o.location, n)
if o.location.Cmp(o.locationMax) > 0 { // more than max
if !o.rollover {
o.last()
return
}
// move forwards from zero
for o.location.Cmp(o.locationMax) > 0 {
o.location.Sub(o.location, o.locationMax)
}
}
o.computeValue()
}

func (o *odometer) Last() {
o.mutex.Lock()
defer o.mutex.Unlock()

o.last()
}

func (o *odometer) SetLocation(n *big.Int) error {
o.mutex.Lock()
defer o.mutex.Unlock()

if n.Cmp(biOne) < 0 || n.Cmp(o.locationMax) > 0 {
return ErrInvalidLocation
}
o.location.Set(n)
o.computeValue()
return nil
}

func (o *odometer) String() string {
o.mutex.Lock()
defer o.mutex.Unlock()

for idx := range o.valueInCharset {
o.valueInCharset[idx] = o.charset[o.value[idx]]
}
return string(o.valueInCharset)
}

func (o *odometer) Value() []int {
o.mutex.Lock()
defer o.mutex.Unlock()

return slices.Clone(o.value)
}

func (o *odometer) computeValue() {
// base conversion: convert the value of location to a decimal with the
// given base using continuous division and use all the remainders as the
// values

// prep the dividend, remainder and modulus
dividend, remainder := new(big.Int).Sub(o.location, biOne), new(big.Int)
// append values in reverse (from right to left)
valIdx := o.length - 1
// append every remainder until dividend becomes zero
for ; dividend.Cmp(biZero) != 0; valIdx-- {
dividend, remainder = dividend.QuoRem(dividend, o.baseBigInt, remainder)
o.value[valIdx] = int(remainder.Int64())
}
// left-pad the remaining characters with 0 (==> 0th char in charset)
for ; valIdx >= 0; valIdx-- {
o.value[valIdx] = 0
}
}

func (o *odometer) decrementAtIndex(idx int) bool {
if o.value[idx] > 0 {
o.value[idx]--
return true
}
if o.value[idx] == 0 && idx > 0 {
o.value[idx] = o.base - 1
o.decrementAtIndex(idx - 1)
return true
}
return false
}

func (o *odometer) first() {
o.location.Set(biOne)
for idx := range o.value {
o.value[idx] = 0
}
}

func (o *odometer) incrementAtIndex(idx int) bool {
if o.value[idx] < o.base-1 {
o.value[idx]++
return true
}
if o.value[idx] == o.base-1 && idx > 0 {
o.value[idx] = 0
o.incrementAtIndex(idx - 1)
return true
}
return false
}

func (o *odometer) last() {
o.location.Set(o.locationMax)
for idx := range o.value {
o.value[idx] = o.base - 1
}
}
83 changes: 83 additions & 0 deletions odometer/odometer_bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package odometer

import (
"math"
"math/big"
"math/rand"
"testing"
"time"

"github.com/jedib0t/go-passwords/charset"
"github.com/stretchr/testify/assert"
)

func BenchmarkOdometer_Big_Decrement(b *testing.B) {
o := New(charset.AllChars, 256)
o.Last()

for i := 0; i < b.N; i++ {
o.Decrement()
}
}

func BenchmarkOdometer_Big_Increment(b *testing.B) {
o := New(charset.AllChars, 256)

for i := 0; i < b.N; i++ {
o.Increment()
}
}

func BenchmarkOdometer_Decrement(b *testing.B) {
o := New(charset.Numbers, 8, WithRolloverEnabled(true))

for i := 0; i < b.N; i++ {
o.Decrement()
}
}

func BenchmarkOdometer_DecrementN(b *testing.B) {
o := New(charset.Numbers, 8, WithRolloverEnabled(true))

n := big.NewInt(5)
for i := 0; i < b.N; i++ {
o.DecrementN(n)
}
}

func BenchmarkOdometer_Increment(b *testing.B) {
o := New(charset.Numbers, 8, WithRolloverEnabled(true))

for i := 0; i < b.N; i++ {
o.Increment()
}
}

func BenchmarkOdometer_IncrementN(b *testing.B) {
o := New(charset.Numbers, 8, WithRolloverEnabled(true))

n := big.NewInt(5)
for i := 0; i < b.N; i++ {
o.IncrementN(n)
}
}

func BenchmarkOdometer_SetLocation(b *testing.B) {
o := New(charset.Numbers, 8, WithRolloverEnabled(true))
maxValues := int64(math.Pow(10, 8))
rng := rand.New(rand.NewSource(time.Now().Unix()))

for i := 0; i < b.N; i++ {
n := big.NewInt(rng.Int63n(maxValues))
err := o.SetLocation(n)
assert.Nil(b, err)
}
}

func BenchmarkOdometer_String(b *testing.B) {
o := New(charset.Numbers, 12)

for i := 0; i < b.N; i++ {
_ = o.String()
}
}
Loading

0 comments on commit 422a86f

Please sign in to comment.