From bb2b164f96c73f28175cf9b433edf0bf9756a83e Mon Sep 17 00:00:00 2001 From: Naveen Mahalingam Date: Sun, 21 Apr 2024 22:06:11 -0700 Subject: [PATCH] sequencer -> odometer --- Makefile | 2 +- README.md | 142 ++++++------ main.go | 72 ++---- odometer/errors.go | 7 + odometer/odometer.go | 269 +++++++++++++++++++++++ odometer/odometer_bench_test.go | 83 +++++++ odometer/odometer_test.go | 268 +++++++++++++++++++++++ odometer/options.go | 15 ++ odometer/utils.go | 14 ++ odometer/utils_test.go | 15 ++ password/generator.go | 2 - password/sequencer/errors.go | 9 - password/sequencer/rules.go | 27 --- password/sequencer/sequencer.go | 313 -------------------------- password/sequencer/sequencer_test.go | 315 --------------------------- password/sequencer/utils.go | 15 -- password/sequencer/utils_test.go | 20 -- 17 files changed, 746 insertions(+), 842 deletions(-) create mode 100644 odometer/errors.go create mode 100644 odometer/odometer.go create mode 100644 odometer/odometer_bench_test.go create mode 100644 odometer/odometer_test.go create mode 100644 odometer/options.go create mode 100644 odometer/utils.go create mode 100644 odometer/utils_test.go delete mode 100644 password/sequencer/errors.go delete mode 100644 password/sequencer/rules.go delete mode 100644 password/sequencer/sequencer.go delete mode 100644 password/sequencer/sequencer_test.go delete mode 100644 password/sequencer/utils.go delete mode 100644 password/sequencer/utils_test.go diff --git a/Makefile b/Makefile index 3e406d2..8ceca36 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ default: test bench: - go test -bench=. -benchmem ./passphrase ./password ./password/sequencer + go test -bench=. -benchmem ./passphrase ./password ./odometer cyclo: gocyclo -over 13 ./*/*.go diff --git a/README.md b/README.md index 670e7c9..2b01793 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,21 @@ Passphrase & Password generation library for GoLang. ## Passphrases + +Passphrases are made up of 2 or more words connected by a separator and may have +capitalized words, and numbers. These are easier for humans to remember compared +to passwords. + +The `passphrase` package helps generate these and supports the following rules +that be used during generation: +* Capitalize words used in the passphrase (foo -> Foo) +* Use a custom dictionary of words instead of built-in English dictionary +* Use X number of Words +* Insert a random number behind one of the words +* Use a custom separator +* Use words with a specific length-range + +### Example ```golang g, err := passphrase.NewGenerator( passphrase.WithCapitalizedWords(true), @@ -41,6 +56,18 @@ Passphrase # 10: "Mirks6-Woofer-Lase" ## Passwords + +Passwords are a random amalgamation of characters. + +The `password` package helps generate these and supports the following rules +that be used during generation: +* Use a specific character-set +* Restrict the length of the password +* Use *at least* X lower-case characters +* Use *at least* X upper-case characters +* Use *at least* X and *at most* Y symbols + +### Example ```golang g, err := password.NewGenerator( password.WithCharset(charset.AllChars.WithoutAmbiguity().WithoutDuplicates()), @@ -72,23 +99,30 @@ Password # 10: "kmQVb&fPqexj" -### Sequential Passwords +## Odometer + +Odometer helps generate all possible string combinations of characters given a +list of characters and the expected length of the string. + +The `odometer` package provides optimal interfaces to move through the list: +* Decrement() +* DecrementN(...) +* Increment() +* IncrementN(...) +* SetLocation(...) +* etc. +### Example ```golang - s, err := sequencer.New( - sequencer.WithCharset(charset.AllChars.WithoutAmbiguity()), - sequencer.WithLength(8), - ) - if err != nil { - panic(err.Error()) - } + o := odometer.New(charset.AlphabetsUpper, 8) + for idx := 1; idx <= 10; idx++ { - fmt.Printf("Password #%3d: %#v\n", idx, s.Get()) + fmt.Printf("Password #%3d: %#v\n", idx, o.String()) - if !s.HasNext() { + if o.AtEnd() { break } - s.Next() + o.Increment() } ```
@@ -102,65 +136,8 @@ Password # 5: "AAAAAAAE" Password # 6: "AAAAAAAF" Password # 7: "AAAAAAAG" Password # 8: "AAAAAAAH" -Password # 9: "AAAAAAAJ" -Password # 10: "AAAAAAAK" - -
- -#### Streamed (for async processing) -```golang - s, err := sequencer.New( - sequencer.WithCharset(charset.Charset("AB")), - sequencer.WithLength(4), - ) - if err != nil { - panic(err.Error()) - } - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) - defer cancel() - - chPasswords := make(chan string, 1) - go func() { - err := s.Stream(ctx, chPasswords) - if err != nil { - panic(err.Error()) - } - }() - - idx := 0 - for { - select { - case <-ctx.Done(): - panic("timed out") - case pw, ok := <-chPasswords: - if !ok { - return - } - idx++ - fmt.Printf("Password #%3d: %#v\n", idx, pw) - } - } -``` -
-Output... -
-Password #  1: "AAAA"
-Password #  2: "AAAB"
-Password #  3: "AABA"
-Password #  4: "AABB"
-Password #  5: "ABAA"
-Password #  6: "ABAB"
-Password #  7: "ABBA"
-Password #  8: "ABBB"
-Password #  9: "BAAA"
-Password # 10: "BAAB"
-Password # 11: "BABA"
-Password # 12: "BABB"
-Password # 13: "BBAA"
-Password # 14: "BBAB"
-Password # 15: "BBBA"
-Password # 16: "BBBB"
+Password #  9: "AAAAAAAI"
+Password # 10: "AAAAAAAJ"
 
@@ -170,27 +147,30 @@ goos: linux goarch: amd64 pkg: github.com/jedib0t/go-passwords/passphrase cpu: AMD Ryzen 9 5900X 12-Core Processor -BenchmarkGenerator_Generate-12 4030634 292.0 ns/op 144 B/op 5 allocs/op +BenchmarkGenerator_Generate-12 3979081 294.8 ns/op 144 B/op 5 allocs/op PASS -ok github.com/jedib0t/go-passwords/passphrase 1.603s +ok github.com/jedib0t/go-passwords/passphrase 1.503s goos: linux goarch: amd64 pkg: github.com/jedib0t/go-passwords/password cpu: AMD Ryzen 9 5900X 12-Core Processor -BenchmarkGenerator_Generate-12 6263398 187.5 ns/op 40 B/op 2 allocs/op +BenchmarkGenerator_Generate-12 5977402 199.4 ns/op 40 B/op 2 allocs/op PASS -ok github.com/jedib0t/go-passwords/password 1.375s +ok github.com/jedib0t/go-passwords/password 1.414s goos: linux goarch: amd64 -pkg: github.com/jedib0t/go-passwords/password/sequencer +pkg: github.com/jedib0t/go-passwords/odometer cpu: AMD Ryzen 9 5900X 12-Core Processor -BenchmarkSequencer_GotoN-12 4355002 274.6 ns/op 32 B/op 3 allocs/op -BenchmarkSequencer_Next-12 13614666 88.99 ns/op 16 B/op 1 allocs/op -BenchmarkSequencer_NextN-12 6216072 187.2 ns/op 32 B/op 3 allocs/op -BenchmarkSequencer_Prev-12 13569340 87.69 ns/op 16 B/op 1 allocs/op -BenchmarkSequencer_PrevN-12 4230654 277.9 ns/op 32 B/op 3 allocs/op +BenchmarkOdometer_Decrement-12 56414820 21.25 ns/op 0 B/op 0 allocs/op +BenchmarkOdometer_Decrement_Big-12 44742920 27.37 ns/op 0 B/op 0 allocs/op +BenchmarkOdometer_DecrementN-12 6536234 177.3 ns/op 16 B/op 2 allocs/op +BenchmarkOdometer_GotoLocation-12 5184144 220.7 ns/op 56 B/op 4 allocs/op +BenchmarkOdometer_Increment-12 61866901 19.37 ns/op 0 B/op 0 allocs/op +BenchmarkOdometer_Increment_Big-12 67560506 17.68 ns/op 0 B/op 0 allocs/op +BenchmarkOdometer_IncrementN-12 7371675 172.7 ns/op 16 B/op 2 allocs/op +BenchmarkOdometer_String-12 14852208 75.40 ns/op 16 B/op 1 allocs/op PASS -ok github.com/jedib0t/go-passwords/password/sequencer 6.888s +ok github.com/jedib0t/go-passwords/odometer 10.282s ``` diff --git a/main.go b/main.go index b536823..a6e2c52 100644 --- a/main.go +++ b/main.go @@ -1,36 +1,30 @@ package main import ( - "context" "fmt" - "time" "github.com/jedib0t/go-passwords/charset" + "github.com/jedib0t/go-passwords/odometer" "github.com/jedib0t/go-passwords/passphrase" "github.com/jedib0t/go-passwords/passphrase/dictionaries" "github.com/jedib0t/go-passwords/password" - "github.com/jedib0t/go-passwords/password/sequencer" ) func main() { fmt.Println("Passphrases:") - passphraseGenerator() + demoPassphraseGenerator() fmt.Println() fmt.Println("Passwords:") - passwordGenerator() + demoPasswordGenerator() fmt.Println() - fmt.Println("Passwords Sequenced:") - passwordSequencer() - fmt.Println() - - fmt.Println("Passwords Sequenced & Streamed:") - passwordSequencerStreaming() + fmt.Println("Odometer:") + demoOdometer() fmt.Println() } -func passphraseGenerator() { +func demoPassphraseGenerator() { g, err := passphrase.NewGenerator( passphrase.WithCapitalizedWords(true), passphrase.WithDictionary(dictionaries.English()), @@ -47,7 +41,7 @@ func passphraseGenerator() { } } -func passwordGenerator() { +func demoPasswordGenerator() { g, err := password.NewGenerator( password.WithCharset(charset.AllChars.WithoutAmbiguity().WithoutDuplicates()), password.WithLength(12), @@ -63,55 +57,15 @@ func passwordGenerator() { } } -func passwordSequencer() { - s, err := sequencer.New( - sequencer.WithCharset(charset.AllChars.WithoutAmbiguity()), - sequencer.WithLength(8), - ) - if err != nil { - panic(err.Error()) - } +func demoOdometer() { + o := odometer.New(charset.AlphabetsUpper, 8) + for idx := 1; idx <= 10; idx++ { - fmt.Printf("Password #%3d: %#v\n", idx, s.Get()) + fmt.Printf("Password #%3d: %#v\n", idx, o.String()) - if !s.HasNext() { + if o.AtEnd() { break } - s.Next() - } -} - -func passwordSequencerStreaming() { - s, err := sequencer.New( - sequencer.WithCharset(charset.Charset("AB")), - sequencer.WithLength(4), - ) - if err != nil { - panic(err.Error()) - } - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) - defer cancel() - - chPasswords := make(chan string, 1) - go func() { - err := s.Stream(ctx, chPasswords) - if err != nil { - panic(err.Error()) - } - }() - - idx := 0 - for { - select { - case <-ctx.Done(): - panic("timed out") - case pw, ok := <-chPasswords: - if !ok { - return - } - idx++ - fmt.Printf("Password #%3d: %#v\n", idx, pw) - } + o.Increment() } } diff --git a/odometer/errors.go b/odometer/errors.go new file mode 100644 index 0000000..7f1a9b8 --- /dev/null +++ b/odometer/errors.go @@ -0,0 +1,7 @@ +package odometer + +import "errors" + +var ( + ErrInvalidLocation = errors.New("invalid location") +) diff --git a/odometer/odometer.go b/odometer/odometer.go new file mode 100644 index 0000000..8fbb645 --- /dev/null +++ b/odometer/odometer.go @@ -0,0 +1,269 @@ +package odometer + +import ( + "math/big" + "sync" + + "github.com/jedib0t/go-passwords/charset" +) + +var ( + biZero = big.NewInt(0) + biOne = big.NewInt(1) +) + +// Odometer defines interfaces to manipulate an Odometer. +type Odometer interface { + // AtEnd returns true if the Odometer is at the last possible value. + AtEnd() bool + // Decrement moves the gears back by one turn. + Decrement() bool + // DecrementN moves the gears back by N turns. + DecrementN(n *big.Int) bool + // First moves the gears to the first possible value. + First() + // GotoLocation moves the gears to a specific location in the list of + // possible locations. The value of 'n' is 1-indexed. + GotoLocation(n *big.Int) error + // Increment moves the gears forward by one turn. + Increment() bool + // IncrementN moves the gears forward by N turns. + IncrementN(n *big.Int) bool + // Last moves the gears to the last possible value. + Last() + // Location returns the current location in the list of possible locations + // and is 1-indexed. + Location() *big.Int + // String returns the value as an end-user would see when they look at an + // Odometer. + String() string +} + +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 +} + +// New returns a new Odometer with "length" gears each containing the given +// Charset as the values. +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) AtEnd() bool { + o.mutex.RLock() + defer o.mutex.RUnlock() + + return o.location.Cmp(o.locationMax) >= 0 +} + +func (o *odometer) Decrement() bool { + o.mutex.Lock() + defer o.mutex.Unlock() + + // set the location + if o.location.Cmp(biOne) == 0 { // at first + if o.rollover { + o.last() + return true + } + return false + } + + // decrement value + o.location.Sub(o.location, biOne) + for idx := o.length - 1; idx >= 0; idx-- { + if o.decrementAtIndex(idx) { + return true + } + } + return true +} + +func (o *odometer) DecrementN(n *big.Int) bool { + 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 false + } + // 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() + return true +} + +func (o *odometer) First() { + o.mutex.Lock() + defer o.mutex.Unlock() + + o.first() +} + +func (o *odometer) Location() *big.Int { + o.mutex.RLock() + defer o.mutex.RUnlock() + + return new(big.Int).Set(o.location) +} + +func (o *odometer) Increment() bool { + 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 true + } + return false + } + + // increment value + o.location.Add(o.location, biOne) + for idx := o.length - 1; idx >= 0; idx-- { + if o.incrementAtIndex(idx) { + return true + } + } + return true +} + +func (o *odometer) IncrementN(n *big.Int) bool { + 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 false + } + // move forwards from zero + for o.location.Cmp(o.locationMax) > 0 { + o.location.Sub(o.location, o.locationMax) + } + } + o.computeValue() + return true +} + +func (o *odometer) Last() { + o.mutex.Lock() + defer o.mutex.Unlock() + + o.last() +} + +func (o *odometer) GotoLocation(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) 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 + } +} diff --git a/odometer/odometer_bench_test.go b/odometer/odometer_bench_test.go new file mode 100644 index 0000000..579429f --- /dev/null +++ b/odometer/odometer_bench_test.go @@ -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_Decrement(b *testing.B) { + o := New(charset.Numbers, 8, WithRolloverEnabled(true)) + + for i := 0; i < b.N; i++ { + _ = o.Decrement() + } +} + +func BenchmarkOdometer_Decrement_Big(b *testing.B) { + o := New(charset.AllChars, 256) + o.Last() + + 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_GotoLocation(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.GotoLocation(n) + assert.Nil(b, err) + } +} + +func BenchmarkOdometer_Increment(b *testing.B) { + o := New(charset.Numbers, 8, WithRolloverEnabled(true)) + + for i := 0; i < b.N; i++ { + _ = o.Increment() + } +} + +func BenchmarkOdometer_Increment_Big(b *testing.B) { + o := New(charset.AllChars, 256) + + 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_String(b *testing.B) { + o := New(charset.Numbers, 12) + + for i := 0; i < b.N; i++ { + _ = o.String() + } +} diff --git a/odometer/odometer_test.go b/odometer/odometer_test.go new file mode 100644 index 0000000..9de8d42 --- /dev/null +++ b/odometer/odometer_test.go @@ -0,0 +1,268 @@ +package odometer + +import ( + "errors" + "math/big" + "testing" + + "github.com/jedib0t/go-passwords/charset" + "github.com/stretchr/testify/assert" +) + +func TestOdometer(t *testing.T) { + t.Run("default", func(t *testing.T) { + o := New(charset.Numbers, 2) + assert.Equal(t, "1", o.Location().String()) + assert.Equal(t, "00", o.String()) + + ok := o.Increment() + assert.True(t, ok) + assert.Equal(t, "2", o.Location().String()) + assert.Equal(t, "01", o.String()) + ok = o.Increment() + assert.True(t, ok) + assert.Equal(t, "3", o.Location().String()) + assert.Equal(t, "02", o.String()) + + o.Last() + assert.Equal(t, "100", o.Location().String()) + assert.Equal(t, "99", o.String()) + ok = o.Increment() + assert.False(t, ok) + assert.Equal(t, "100", o.Location().String()) + assert.Equal(t, "99", o.String()) + + ok = o.Decrement() + assert.True(t, ok) + assert.Equal(t, "99", o.Location().String()) + assert.Equal(t, "98", o.String()) + ok = o.Decrement() + assert.True(t, ok) + assert.Equal(t, "98", o.Location().String()) + assert.Equal(t, "97", o.String()) + + o.First() + assert.Equal(t, "1", o.Location().String()) + assert.Equal(t, "00", o.String()) + ok = o.Decrement() + assert.False(t, ok) + assert.Equal(t, "1", o.Location().String()) + assert.Equal(t, "00", o.String()) + }) + + t.Run("rollover", func(t *testing.T) { + o := New(charset.Numbers, 2, WithRolloverEnabled(true)) + assert.Equal(t, "1", o.Location().String()) + assert.Equal(t, "00", o.String()) + + ok := o.Increment() + assert.True(t, ok) + assert.Equal(t, "2", o.Location().String()) + assert.Equal(t, "01", o.String()) + ok = o.Increment() + assert.True(t, ok) + assert.Equal(t, "3", o.Location().String()) + assert.Equal(t, "02", o.String()) + + o.Last() + assert.Equal(t, "100", o.Location().String()) + assert.Equal(t, "99", o.String()) + ok = o.Increment() + assert.True(t, ok) + assert.Equal(t, "1", o.Location().String()) + assert.Equal(t, "00", o.String()) + ok = o.Increment() + assert.True(t, ok) + assert.Equal(t, "2", o.Location().String()) + assert.Equal(t, "01", o.String()) + + ok = o.Decrement() + assert.True(t, ok) + assert.Equal(t, "1", o.Location().String()) + assert.Equal(t, "00", o.String()) + ok = o.Decrement() + assert.True(t, ok) + assert.Equal(t, "100", o.Location().String()) + assert.Equal(t, "99", o.String()) + ok = o.Decrement() + assert.True(t, ok) + assert.Equal(t, "99", o.Location().String()) + assert.Equal(t, "98", o.String()) + }) + + t.Run("really big odometer", func(t *testing.T) { + o := New(charset.AllChars, 256) + assert.Equal(t, "1", o.Location().String()) + assert.Equal(t, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", o.String()) + + o.Last() + assert.Equal(t, "****************************************************************************************************************************************************************************************************************************************************************", o.String()) + assert.Equal(t, "22135954000460481554501886154749459371625170502600730699163663905247049740079899968480034338379403807827944552623126075988673634259405600148560278663819464589512058373791164736632467335096807212642462431896323483136010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", o.Location().String()) + ok := o.Decrement() + assert.True(t, ok) + assert.Equal(t, "***************************************************************************************************************************************************************************************************************************************************************&", o.String()) + assert.Equal(t, "22135954000460481554501886154749459371625170502600730699163663905247049740079899968480034338379403807827944552623126075988673634259405600148560278663819464589512058373791164736632467335096807212642462431896323483136009999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999", o.Location().String()) + ok = o.Decrement() + assert.True(t, ok) + assert.Equal(t, "***************************************************************************************************************************************************************************************************************************************************************^", o.String()) + assert.Equal(t, "22135954000460481554501886154749459371625170502600730699163663905247049740079899968480034338379403807827944552623126075988673634259405600148560278663819464589512058373791164736632467335096807212642462431896323483136009999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999998", o.Location().String()) + + o.First() + assert.Equal(t, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", o.String()) + assert.Equal(t, "1", o.Location().String()) + ok = o.Increment() + assert.True(t, ok) + assert.Equal(t, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB", o.String()) + assert.Equal(t, "2", o.Location().String()) + ok = o.Increment() + assert.True(t, ok) + assert.Equal(t, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC", o.String()) + assert.Equal(t, "3", o.Location().String()) + }) +} + +func TestOdometer_Decrement(t *testing.T) { + o := New(charset.Numbers, 3) + assert.Equal(t, "1", o.Location().String()) + assert.Equal(t, "000", o.String()) + + err := o.GotoLocation(big.NewInt(1000)) + assert.Nil(t, err) + assert.Equal(t, "999", o.String()) + + for idx := int64(999); idx >= 1; idx-- { + ok := o.Decrement() + assert.True(t, ok) + assert.Equal(t, big.NewInt(idx), o.Location(), idx) + } +} + +func TestOdometer_DecrementN(t *testing.T) { + t.Run("default", func(t *testing.T) { + o := New(charset.Numbers, 2) + assert.Equal(t, "1", o.Location().String()) + assert.Equal(t, "00", o.String()) + + ok := o.DecrementN(big.NewInt(5)) + assert.False(t, ok) + assert.Equal(t, "1", o.Location().String()) + assert.Equal(t, "00", o.String()) + + o.Last() + assert.Equal(t, "100", o.Location().String()) + assert.Equal(t, "99", o.String()) + + ok = o.DecrementN(big.NewInt(5)) + assert.True(t, ok) + assert.Equal(t, "95", o.Location().String()) + assert.Equal(t, "94", o.String()) + + ok = o.DecrementN(big.NewInt(500)) + assert.False(t, ok) + assert.Equal(t, "1", o.Location().String()) + assert.Equal(t, "00", o.String()) + }) + + t.Run("rollover", func(t *testing.T) { + o := New(charset.Numbers, 2, WithRolloverEnabled(true)) + assert.Equal(t, "1", o.Location().String()) + assert.Equal(t, "00", o.String()) + + ok := o.DecrementN(big.NewInt(1)) + assert.True(t, ok) + assert.Equal(t, "100", o.Location().String()) + assert.Equal(t, "99", o.String()) + + ok = o.DecrementN(big.NewInt(5)) + assert.True(t, ok) + assert.Equal(t, "95", o.Location().String()) + assert.Equal(t, "94", o.String()) + + ok = o.DecrementN(big.NewInt(500)) + assert.True(t, ok) + assert.Equal(t, "95", o.Location().String()) + assert.Equal(t, "94", o.String()) + }) +} + +func TestOdometer_GotoLocation(t *testing.T) { + o := New(charset.Numbers, 2) + assert.Equal(t, "1", o.Location().String()) + assert.Equal(t, "00", o.String()) + + err := o.GotoLocation(big.NewInt(0)) + assert.NotNil(t, err) + assert.True(t, errors.Is(err, ErrInvalidLocation)) + + err = o.GotoLocation(big.NewInt(5)) + assert.Nil(t, err) + assert.Equal(t, "5", o.Location().String()) + assert.Equal(t, "04", o.String()) + + err = o.GotoLocation(big.NewInt(50)) + assert.Nil(t, err) + assert.Equal(t, "50", o.Location().String()) + assert.Equal(t, "49", o.String()) + + err = o.GotoLocation(big.NewInt(100)) + assert.Nil(t, err) + assert.Equal(t, "100", o.Location().String()) + assert.Equal(t, "99", o.String()) + + err = o.GotoLocation(big.NewInt(101)) + assert.NotNil(t, err) + assert.True(t, errors.Is(err, ErrInvalidLocation)) +} + +func TestOdometer_Increment(t *testing.T) { + o := New(charset.Numbers, 3) + assert.Equal(t, "1", o.Location().String()) + + for idx := int64(2); idx <= 1000; idx++ { + ok := o.Increment() + assert.True(t, ok) + assert.Equal(t, big.NewInt(idx), o.Location(), idx) + } +} + +func TestOdometer_IncrementN(t *testing.T) { + t.Run("default", func(t *testing.T) { + o := New(charset.Numbers, 2) + assert.Equal(t, "1", o.Location().String()) + assert.Equal(t, "00", o.String()) + + ok := o.IncrementN(big.NewInt(5)) + assert.True(t, ok) + assert.Equal(t, "6", o.Location().String()) + assert.Equal(t, "05", o.String()) + + o.Last() + assert.Equal(t, "100", o.Location().String()) + assert.Equal(t, "99", o.String()) + + ok = o.IncrementN(big.NewInt(5)) + assert.False(t, ok) + assert.Equal(t, "100", o.Location().String()) + assert.Equal(t, "99", o.String()) + }) + + t.Run("rollover", func(t *testing.T) { + o := New(charset.Numbers, 2, WithRolloverEnabled(true)) + assert.Equal(t, "1", o.Location().String()) + assert.Equal(t, "00", o.String()) + + ok := o.IncrementN(big.NewInt(5)) + assert.True(t, ok) + assert.Equal(t, "6", o.Location().String()) + assert.Equal(t, "05", o.String()) + + o.Last() + assert.Equal(t, "100", o.Location().String()) + assert.Equal(t, "99", o.String()) + + ok = o.IncrementN(big.NewInt(5)) + assert.True(t, ok) + assert.Equal(t, "5", o.Location().String()) + assert.Equal(t, "04", o.String()) + }) +} diff --git a/odometer/options.go b/odometer/options.go new file mode 100644 index 0000000..98ac6ab --- /dev/null +++ b/odometer/options.go @@ -0,0 +1,15 @@ +package odometer + +type Option func(o *odometer) + +var ( + defaultOptions = []Option{ + WithRolloverEnabled(false), + } +) + +func WithRolloverEnabled(r bool) Option { + return func(o *odometer) { + o.rollover = r + } +} diff --git a/odometer/utils.go b/odometer/utils.go new file mode 100644 index 0000000..46ea98d --- /dev/null +++ b/odometer/utils.go @@ -0,0 +1,14 @@ +package odometer + +import ( + "math/big" +) + +func numValues(base int, length int) *big.Int { + if length == 0 { + return big.NewInt(0) + } + + i, e := big.NewInt(int64(base)), big.NewInt(int64(length)) + return i.Exp(i, e, nil) +} diff --git a/odometer/utils_test.go b/odometer/utils_test.go new file mode 100644 index 0000000..eca110d --- /dev/null +++ b/odometer/utils_test.go @@ -0,0 +1,15 @@ +package odometer + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_numValues(t *testing.T) { + assert.Equal(t, "0", numValues(10, 0).String()) + assert.Equal(t, "10", numValues(10, 1).String()) + assert.Equal(t, "10000", numValues(10, 4).String()) + assert.Equal(t, "100000000", numValues(10, 8).String()) + assert.Equal(t, "10000000000", numValues(10, 10).String()) +} diff --git a/password/generator.go b/password/generator.go index b7e4dec..fc96400 100644 --- a/password/generator.go +++ b/password/generator.go @@ -113,8 +113,6 @@ func (g *generator) numSymbolsToGenerate() int { return 0 } -func (g *generator) ruleEnforcer() {} - func (g *generator) sanitize() (Generator, error) { // validate the inputs if len(g.charset) == 0 { diff --git a/password/sequencer/errors.go b/password/sequencer/errors.go deleted file mode 100644 index 5164658..0000000 --- a/password/sequencer/errors.go +++ /dev/null @@ -1,9 +0,0 @@ -package sequencer - -import "errors" - -var ( - ErrEmptyCharset = errors.New("cannot generate passwords with empty charset") - ErrInvalidN = errors.New("value of N exceeds valid range") - ErrZeroLenPassword = errors.New("cannot generate passwords with 0 length") -) diff --git a/password/sequencer/rules.go b/password/sequencer/rules.go deleted file mode 100644 index 82070b3..0000000 --- a/password/sequencer/rules.go +++ /dev/null @@ -1,27 +0,0 @@ -package sequencer - -import "github.com/jedib0t/go-passwords/charset" - -// Rule controls how the Generator/Sequencer generates passwords. -type Rule func(s *sequencer) - -var ( - basicRules = []Rule{ - WithCharset(charset.AlphaNumeric), - WithLength(12), - } -) - -// WithCharset sets the Charset the Generator/Sequencer can use. -func WithCharset(c charset.Charset) Rule { - return func(s *sequencer) { - s.charset = []rune(c) - } -} - -// WithLength sets the length of the generated password. -func WithLength(l int) Rule { - return func(s *sequencer) { - s.numChars = l - } -} diff --git a/password/sequencer/sequencer.go b/password/sequencer/sequencer.go deleted file mode 100644 index e9ece08..0000000 --- a/password/sequencer/sequencer.go +++ /dev/null @@ -1,313 +0,0 @@ -package sequencer - -import ( - "context" - "fmt" - "math/big" - "math/rand/v2" - "sync" - - "github.com/jedib0t/go-passwords/charset" -) - -var ( - biZero = big.NewInt(0) - biOne = big.NewInt(1) -) - -// Sequencer is a deterministic Password Generator that generates all possible -// combinations of passwords for a Charset and defined number of characters in -// the password. It lets you move back and forth through the list of possible -// passwords, and involves no RNG. -type Sequencer interface { - // First moves to the first possible password and returns the same. - First() string - // Get returns the current password in the sequence. - Get() string - // GetN returns the value for N (location in list of possible passwords). - GetN() *big.Int - // GotoN overrides N. - GotoN(n *big.Int) (string, error) - // HasNext returns true if there is at least one more password. - HasNext() bool - // Last moves to the last possible password and returns the same. - Last() string - // Next moves to the next possible password and returns the same. - Next() string - // NextN is like Next looped N times, in an optimal way. - NextN(n *big.Int) string - // Prev moves to the previous possible password and returns the same. - Prev() string - // PrevN is like Prev looped N times, in an optimal way. - PrevN(n *big.Int) string - // Reset cleans up state and moves to the first possible word. - Reset() - // Stream sends all possible passwords in order to the given channel. If you - // want to limit output, pass in a *big.Int with the number of passwords you - // want to be generated and streamed. - Stream(ctx context.Context, ch chan string, optionalCount ...*big.Int) error -} - -type sequencer struct { - base *big.Int - charset []rune - charsetLen int - charsetMaxIdx int - maxWords *big.Int - mutex sync.Mutex - n *big.Int - nMax *big.Int - numChars int - password []int - passwordChars []rune - passwordMaxIdx int - rng *rand.Rand -} - -// New returns a password Sequencer. -func New(rules ...Rule) (Sequencer, error) { - s := &sequencer{} - for _, rule := range append(basicRules, rules...) { - rule(s) - } - - // init the variables - s.base = big.NewInt(int64(len(s.charset))) - s.charsetLen = len(s.charset) - s.charsetMaxIdx = len(s.charset) - 1 - s.maxWords = MaximumPossibleWords(charset.Charset(s.charset), s.numChars) - s.n = big.NewInt(0) - s.nMax = new(big.Int).Sub(s.maxWords, biOne) - s.password = make([]int, s.numChars) - s.passwordChars = make([]rune, s.numChars) - s.passwordMaxIdx = s.numChars - 1 - - if len(s.charset) == 0 { - return nil, ErrEmptyCharset - } - if s.numChars <= 0 { - return nil, ErrZeroLenPassword - } - return s, nil -} - -// First moves to the first possible password and returns the same. -func (s *sequencer) First() string { - s.mutex.Lock() - defer s.mutex.Unlock() - - s.n.Set(biZero) - for idx := range s.password { - s.password[idx] = 0 - } - return s.get() -} - -// Get returns the current password in the sequence. -func (s *sequencer) Get() string { - s.mutex.Lock() - defer s.mutex.Unlock() - - return s.get() -} - -// GetN returns the current location in the list of possible passwords. -func (s *sequencer) GetN() *big.Int { - s.mutex.Lock() - defer s.mutex.Unlock() - - return new(big.Int).Set(s.n) -} - -// GotoN overrides the current location in the list of possible passwords. -func (s *sequencer) GotoN(n *big.Int) (string, error) { - s.mutex.Lock() - defer s.mutex.Unlock() - - // ensure n is within possible range (0 to nMax) - if n.Sign() < 0 || n.Cmp(s.nMax) > 0 { - return "", fmt.Errorf("%w: n=%s, range=[0 to %s]", ErrInvalidN, n, s.nMax) - } - - // override and compute the word - s.n.Set(n) - s.computeWord() - return s.get(), nil -} - -// HasNext returns true if there is at least one more password. -func (s *sequencer) HasNext() bool { - s.mutex.Lock() - defer s.mutex.Unlock() - - return s.n.Cmp(s.nMax) < 0 -} - -// Last moves to the last possible password and returns the same. -func (s *sequencer) Last() string { - s.mutex.Lock() - defer s.mutex.Unlock() - - s.n.Set(s.nMax) - for idx := range s.password { - s.password[idx] = s.charsetMaxIdx - } - return s.get() -} - -// Next moves to the next possible password and returns the same. -func (s *sequencer) Next() string { - s.mutex.Lock() - defer s.mutex.Unlock() - - s.next() - return s.get() -} - -// NextN is like Next looped N times, in an optimal way. -func (s *sequencer) NextN(n *big.Int) string { - s.mutex.Lock() - defer s.mutex.Unlock() - - if s.n.Cmp(s.nMax) < 0 { - s.n.Add(s.n, n) - s.computeWord() - } - return s.get() -} - -// Prev moves to the previous possible password and returns the same. -func (s *sequencer) Prev() string { - s.mutex.Lock() - defer s.mutex.Unlock() - - s.prev() - return s.get() -} - -// PrevN is like Prev looped N times, in an optimal way. -func (s *sequencer) PrevN(n *big.Int) string { - s.mutex.Lock() - defer s.mutex.Unlock() - - if s.n.Cmp(biZero) > 0 { - s.n.Sub(s.n, n) - s.computeWord() - } - return s.get() -} - -// Reset cleans up state and moves to the first possible word. -func (s *sequencer) Reset() { - s.First() -} - -// Stream sends all possible passwords in order to the given channel. If you -// want to limit output, pass in a *big.Int with the number of passwords you -// want to be generated and streamed. -func (s *sequencer) Stream(ctx context.Context, ch chan string, optionalCount ...*big.Int) error { - defer close(ch) - - maxToBeSent := new(big.Int).Set(s.maxWords) - if len(optionalCount) == 1 && optionalCount[0] != nil && optionalCount[0].Cmp(biZero) > 0 { - maxToBeSent.Set(optionalCount[0]) - } - - s.mutex.Lock() - defer s.mutex.Unlock() - - ch <- s.get() - chSent := big.NewInt(1) - for ; s.next() && chSent.Cmp(maxToBeSent) < 0; chSent.Add(chSent, biOne) { - select { - case <-ctx.Done(): - return ctx.Err() - default: - ch <- s.get() - } - } - return nil -} - -func (s *sequencer) computeWord() { - // base conversion: convert the value of n to a decimal with a base of - // wg.charsetLen using continuous division and use all the remainders as the - // index value to pick the character from the charset - - // prep the dividend, remainder and modulus - dividend, remainder := new(big.Int).Set(s.n), new(big.Int) - // append values to the password in reverse (from right to left) - charIdx := s.passwordMaxIdx - // append every remainder until dividend becomes zero - for ; dividend.Cmp(biZero) != 0; charIdx-- { - dividend, remainder = dividend.QuoRem(dividend, s.base, remainder) - s.password[charIdx] = int(remainder.Int64()) - } - // left-pad the remaining characters with 0 (==> 0th char in charset) - for ; charIdx >= 0; charIdx-- { - s.password[charIdx] = 0 - } -} - -func (s *sequencer) get() string { - for idx := range s.passwordChars { - s.passwordChars[idx] = s.charset[s.password[idx]] - } - return string(s.passwordChars) -} - -func (s *sequencer) next() bool { - if s.n.Cmp(s.nMax) >= 0 { - return false - } - - s.n.Add(s.n, biOne) - for idx := s.passwordMaxIdx; idx >= 0; idx-- { - if s.nextAtIndex(idx) { - return true - } - } - return true -} - -func (s *sequencer) nextAtIndex(idx int) bool { - if s.password[idx] < s.charsetMaxIdx { - s.password[idx]++ - return true - } - if s.password[idx] == s.charsetMaxIdx && idx > 0 { - s.password[idx] = 0 - s.nextAtIndex(idx - 1) - return true - } - return false -} - -func (s *sequencer) prev() bool { - if s.n.Cmp(biZero) <= 0 { - return false - } - - s.n.Sub(s.n, biOne) - for idx := s.passwordMaxIdx; idx >= 0; idx-- { - if s.prevAtIndex(idx) { - return true - } - } - return true -} - -func (s *sequencer) prevAtIndex(idx int) bool { - if s.password[idx] > 0 { - s.password[idx]-- - return true - } - if s.password[idx] == 0 && idx > 0 { - s.password[idx] = s.charsetMaxIdx - s.prevAtIndex(idx - 1) - return true - } - return false -} - -func (s *sequencer) ruleEnforcer() {} diff --git a/password/sequencer/sequencer_test.go b/password/sequencer/sequencer_test.go deleted file mode 100644 index c3b42e7..0000000 --- a/password/sequencer/sequencer_test.go +++ /dev/null @@ -1,315 +0,0 @@ -package sequencer - -import ( - "context" - "errors" - "fmt" - "math" - "math/big" - "testing" - "time" - - "github.com/jedib0t/go-passwords/charset" - "github.com/stretchr/testify/assert" -) - -func BenchmarkSequencer_GotoN(b *testing.B) { - s, err := New( - WithCharset(charset.AlphaNumeric.WithoutAmbiguity().WithoutDuplicates()), - WithLength(12), - ) - assert.Nil(b, err) - - n := big.NewInt(math.MaxInt) - pw, err := s.GotoN(n) - assert.NotEmpty(b, pw) - assert.Nil(b, err) - assert.Equal(b, "AXZvFwUyHzQM", pw) - - for idx := 0; idx < b.N; idx++ { - _, _ = s.GotoN(n) - } -} - -func BenchmarkSequencer_Next(b *testing.B) { - s, err := New( - WithCharset(charset.AlphaNumeric.WithoutAmbiguity().WithoutDuplicates()), - WithLength(12), - ) - assert.Nil(b, err) - s.First() - - assert.NotEmpty(b, s.Next()) - for idx := 0; idx < b.N; idx++ { - s.Next() - } -} - -func BenchmarkSequencer_NextN(b *testing.B) { - s, err := New( - WithCharset(charset.AlphaNumeric.WithoutAmbiguity().WithoutDuplicates()), - WithLength(12), - ) - assert.Nil(b, err) - s.First() - - n := big.NewInt(100) - assert.NotEmpty(b, s.NextN(n)) - for idx := 0; idx < b.N; idx++ { - s.NextN(n) - } -} - -func BenchmarkSequencer_Prev(b *testing.B) { - s, err := New( - WithCharset(charset.AlphaNumeric.WithoutAmbiguity().WithoutDuplicates()), - WithLength(12), - ) - assert.Nil(b, err) - s.Last() - - assert.NotEmpty(b, s.Prev()) - for idx := 0; idx < b.N; idx++ { - s.Prev() - } -} - -func BenchmarkSequencer_PrevN(b *testing.B) { - s, err := New( - WithCharset(charset.AlphaNumeric.WithoutAmbiguity().WithoutDuplicates()), - WithLength(12), - ) - assert.Nil(b, err) - _, _ = s.GotoN(big.NewInt(math.MaxInt)) - - n := big.NewInt(100) - assert.NotEmpty(b, s.PrevN(n)) - for idx := 0; idx < b.N; idx++ { - s.PrevN(n) - } -} - -func TestSequencer(t *testing.T) { - s, err := New( - WithCharset(""), - WithLength(3), - ) - assert.Nil(t, s) - assert.NotNil(t, err) - assert.True(t, errors.Is(err, ErrEmptyCharset)) - s, err = New( - WithCharset("AB"), - WithLength(0), - ) - assert.Nil(t, s) - assert.NotNil(t, err) - assert.True(t, errors.Is(err, ErrZeroLenPassword)) - - s, err = New( - WithCharset("AB"), - WithLength(3), - ) - assert.Nil(t, err) - assert.Equal(t, "AAA", s.Get()) - assert.Equal(t, "0", s.GetN().String()) - assert.Equal(t, "AAA", s.First()) - assert.Equal(t, "0", s.GetN().String()) - assert.True(t, s.HasNext()) - assert.Equal(t, "BAA", s.NextN(big.NewInt(4))) - assert.Equal(t, "4", s.GetN().String()) - assert.True(t, s.HasNext()) - assert.Equal(t, "BBA", s.NextN(big.NewInt(2))) - assert.Equal(t, "6", s.GetN().String()) - assert.True(t, s.HasNext()) - assert.Equal(t, "BAA", s.PrevN(big.NewInt(2))) - assert.Equal(t, "4", s.GetN().String()) - assert.True(t, s.HasNext()) - assert.Equal(t, "AAA", s.PrevN(big.NewInt(4))) - assert.Equal(t, "0", s.GetN().String()) - assert.True(t, s.HasNext()) - assert.Equal(t, "BBB", s.Last()) - assert.Equal(t, "7", s.GetN().String()) - assert.False(t, s.HasNext()) - assert.Equal(t, "BBB", s.Get()) - - // Next() - expectedAnswers := []string{ - "AAA", - "AAB", - "ABA", - "ABB", - "BAA", - "BAB", - "BBA", - "BBB", - } - s.Reset() - assert.Equal(t, expectedAnswers[0], s.Get()) - for idx := 1; idx < len(expectedAnswers); idx++ { - assert.Equal(t, expectedAnswers[idx], s.Next()) - } - assert.Equal(t, "BBB", s.Next()) - assert.Equal(t, "BBB", s.NextN(big.NewInt(303))) - - // Prev() - expectedAnswers = []string{ - "BBB", - "BBA", - "BAB", - "BAA", - "ABB", - "ABA", - "AAB", - "AAA", - } - s.Last() - assert.Equal(t, expectedAnswers[0], s.Get()) - for idx := 1; idx < len(expectedAnswers); idx++ { - assert.Equal(t, expectedAnswers[idx], s.Prev()) - } - assert.Equal(t, "AAA", s.Prev()) - assert.Equal(t, "AAA", s.PrevN(big.NewInt(303))) -} - -func TestSequencer_GotoN(t *testing.T) { - s, err := New( - WithCharset("AB"), - WithLength(3), - ) - assert.Nil(t, err) - - pw, err := s.GotoN(big.NewInt(-1)) - assert.NotNil(t, err) - assert.True(t, errors.Is(err, ErrInvalidN)) - pw, err = s.GotoN(big.NewInt(100)) - assert.NotNil(t, err) - assert.True(t, errors.Is(err, ErrInvalidN)) - - s, err = New( - WithCharset("AB"), - WithLength(4), - ) - assert.Nil(t, err) - expectedPasswords := []string{ - "AAAA", - "AAAB", - "AABA", - "AABB", - "ABAA", - "ABAB", - "ABBA", - "ABBB", - "BAAA", - "BAAB", - "BABA", - "BABB", - "BBAA", - "BBAB", - "BBBA", - "BBBB", - } - for idx := 0; idx < len(expectedPasswords); idx++ { - pw, err = s.GotoN(big.NewInt(int64(idx))) - assert.Nil(t, err) - assert.Equal(t, expectedPasswords[idx], pw, fmt.Sprintf("idx=%d", idx)) - } -} - -func TestSequencer_Stream(t *testing.T) { - s, err := New( - WithCharset("AB"), - WithLength(3), - ) - assert.Nil(t, err) - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) - defer cancel() - - ch := make(chan string, 1) - // streams all possible passwords into the channel in an async routine - go func() { - //t.Logf("< streaming passwords ...") - err2 := s.Stream(ctx, ch) - assert.Nil(t, err2) - //t.Logf("< streaming passwords ... done!") - }() - - // listen on channel for passwords until channel is closed, or until timeout - pw, ok := "", true - var passwords []string - //t.Logf("> receiving passwords ...") - for ok { - select { - case <-ctx.Done(): - assert.Fail(t, ctx.Err().Error()) - ok = false - case pw, ok = <-ch: - if ok { - //t.Logf("> ++ received %#v", pw) - passwords = append(passwords, pw) - } - } - } - //t.Logf("> receiving passwords ... done!") - - // verify received passwords - expectedPasswords := []string{ - "AAA", - "AAB", - "ABA", - "ABB", - "BAA", - "BAB", - "BBA", - "BBB", - } - assert.Equal(t, expectedPasswords, passwords) -} - -func TestSequencer_Stream_Limited(t *testing.T) { - s, err := New( - WithCharset("AB"), - WithLength(3), - ) - assert.Nil(t, err) - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) - defer cancel() - - ch := make(chan string, 1) - // streams all possible passwords into the channel in an async routine - go func() { - //t.Logf("< streaming passwords ...") - err2 := s.Stream(ctx, ch, big.NewInt(5)) - assert.Nil(t, err2) - //t.Logf("< streaming passwords ... done!") - }() - - // listen on channel for passwords until channel is closed, or until timeout - pw, ok := "", true - var passwords []string - //t.Logf("> receiving passwords ...") - for ok { - select { - case <-ctx.Done(): - assert.Fail(t, ctx.Err().Error()) - ok = false - case pw, ok = <-ch: - if ok { - //t.Logf("> ++ received %#v", pw) - passwords = append(passwords, pw) - } - } - } - //t.Logf("> receiving passwords ... done!") - - // verify received passwords - expectedPasswords := []string{ - "AAA", - "AAB", - "ABA", - "ABB", - "BAA", - } - assert.Equal(t, expectedPasswords, passwords) -} diff --git a/password/sequencer/utils.go b/password/sequencer/utils.go deleted file mode 100644 index 87d9fc1..0000000 --- a/password/sequencer/utils.go +++ /dev/null @@ -1,15 +0,0 @@ -package sequencer - -import ( - "math/big" - - "github.com/jedib0t/go-passwords/charset" -) - -// MaximumPossibleWords returns the maximum number of unique passwords that can -// be generated with the given Charset and the number of characters allowed in -// the password. -func MaximumPossibleWords(charset charset.Charset, numChars int) *big.Int { - i, e := big.NewInt(int64(len(charset))), big.NewInt(int64(numChars)) - return i.Exp(i, e, nil) -} diff --git a/password/sequencer/utils_test.go b/password/sequencer/utils_test.go deleted file mode 100644 index 894765a..0000000 --- a/password/sequencer/utils_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package sequencer - -import ( - "testing" - - "github.com/jedib0t/go-passwords/charset" - "github.com/stretchr/testify/assert" -) - -func TestMaximumPossibleWords(t *testing.T) { - assert.Equal(t, "10", MaximumPossibleWords(charset.Numbers, 1).String()) - assert.Equal(t, "10000", MaximumPossibleWords(charset.Numbers, 4).String()) - assert.Equal(t, "100000000", MaximumPossibleWords(charset.Numbers, 8).String()) - assert.Equal(t, "16777216", MaximumPossibleWords(charset.Symbols, 8).String()) - assert.Equal(t, "377801998336", MaximumPossibleWords(charset.SymbolsFull, 8).String()) - assert.Equal(t, "53459728531456", MaximumPossibleWords(charset.Alphabets, 8).String()) - assert.Equal(t, "218340105584896", MaximumPossibleWords(charset.AlphaNumeric, 8).String()) - assert.Equal(t, "576480100000000", MaximumPossibleWords(charset.AllChars, 8).String()) - assert.Equal(t, "4304672100000000", MaximumPossibleWords(charset.AlphaNumeric+charset.SymbolsFull, 8).String()) -}