Skip to content

Commit ecc4bb4

Browse files
committed
passit: add RepeatGen
1 parent 4e09c9b commit ecc4bb4

File tree

3 files changed

+97
-14
lines changed

3 files changed

+97
-14
lines changed

README.md

+12-11
Original file line numberDiff line numberDiff line change
@@ -82,17 +82,18 @@ The package also provides a number of generators that produce output based on us
8282

8383
There are also a number of 'helper' generators that interact with the output of other generators:
8484

85-
| Generator | Description |
86-
| ----------------- | ------------------------------------------------------------------------------- |
87-
| `Alternate` | Select a generator at random |
88-
| `Join` | Concatenate the output of multiple generators |
89-
| `Repeat` | Invoke a generator multiple times and concatenate the output |
90-
| `RandomRepeat` | Invoke a generator a random number of times and concatenate the output |
91-
| `RejectionSample` | Continually invoke a generator until the output passes a test |
92-
| `Transform` | Invoke a generator and convert the output according to a user supplied function |
93-
| `LowerCase` | Invoke a generator and convert the output to lower case |
94-
| `UpperCase` | Invoke a generator and convert the output to upper case |
95-
| `TitleCase` | Invoke a generator and convert the output to language-specific title case |
85+
| Generator | Description |
86+
| ----------------- | ------------------------------------------------------------------------------------- |
87+
| `Alternate` | Select a generator at random |
88+
| `Join` | Concatenate the output of multiple generators |
89+
| `Repeat` | Invoke a generator multiple times and concatenate the output with a fixed separator |
90+
| `RepeatGen` | Invoke a generator multiple times and concatenate the output with a dynamic separator |
91+
| `RandomRepeat` | Invoke a generator a random number of times and concatenate the output |
92+
| `RejectionSample` | Continually invoke a generator until the output passes a test |
93+
| `Transform` | Invoke a generator and convert the output according to a user supplied function |
94+
| `LowerCase` | Invoke a generator and convert the output to lower case |
95+
| `UpperCase` | Invoke a generator and convert the output to upper case |
96+
| `TitleCase` | Invoke a generator and convert the output to language-specific title case |
9697

9798
Most generators only generate a single of something, be it a rune, ASCII character
9899
or word. For generating longer passwords use `Repeat` or `RandomRepeat`, possibly

helper.go

+47-3
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,8 @@ type repeatGenerator struct {
4545
count int
4646
}
4747

48-
// Repeat returns a Generator that concatenates the output of invoking the Generator
49-
// count times to create a single string. The separator string sep is placed between
50-
// the outputs in the resulting string.
48+
// Repeat returns a Generator that invokes the Generator count times and
49+
// concatenates the output with a fixed separator.
5150
func Repeat(gen Generator, sep string, count int) Generator {
5251
switch {
5352
case count < 0:
@@ -75,6 +74,51 @@ func (rg *repeatGenerator) Password(r io.Reader) (string, error) {
7574
return strings.Join(parts, rg.sep), nil
7675
}
7776

77+
type repeatGenGenerator struct {
78+
gen Generator
79+
sep Generator
80+
count int
81+
}
82+
83+
// RepeatGen returns a Generator that invokes gen count times and concatenates the
84+
// output with a dynamic separator produced by sep.
85+
//
86+
// For instance, RepeatGen(gen, sep, 4) would be equivalent to
87+
// Join("", gen, sep, gen, sep, gen, sep, gen).
88+
func RepeatGen(gen, sep Generator, count int) Generator {
89+
switch {
90+
case count < 0:
91+
panic("passit: count must be positive")
92+
case count == 0:
93+
return Empty
94+
case count == 1:
95+
return gen
96+
default:
97+
return &repeatGenGenerator{gen, sep, count}
98+
}
99+
}
100+
101+
func (rg *repeatGenGenerator) Password(r io.Reader) (string, error) {
102+
var b strings.Builder
103+
for i := range rg.count {
104+
if i > 0 {
105+
sep, err := rg.sep.Password(r)
106+
if err != nil {
107+
return "", err
108+
}
109+
b.WriteString(sep)
110+
}
111+
112+
part, err := rg.gen.Password(r)
113+
if err != nil {
114+
return "", err
115+
}
116+
b.WriteString(part)
117+
}
118+
119+
return b.String(), nil
120+
}
121+
78122
type randomRepeatGenerator struct {
79123
gen Generator
80124
sep string

helper_test.go

+38
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"strings"
1212
"testing"
1313
"testing/iotest"
14+
"unicode"
1415
"unicode/utf8"
1516

1617
"github.com/stretchr/testify/assert"
@@ -113,6 +114,43 @@ func TestRepeat(t *testing.T) {
113114
}
114115
}
115116

117+
func TestRepeatGen(t *testing.T) {
118+
assert.PanicsWithValue(t, "passit: count must be positive", func() {
119+
RepeatGen(Hyphen, Space, -1)
120+
})
121+
122+
assert.Equal(t, Empty, RepeatGen(Hyphen, Space, 0),
123+
"Repeat with count zero should return Empty")
124+
125+
assert.Equal(t, Hyphen, RepeatGen(Hyphen, Space, 1),
126+
"Repeat with count one should return Generator")
127+
128+
for _, tc := range []struct {
129+
count int
130+
sep Generator
131+
expect string
132+
}{
133+
{2, Space, "reprint wool"},
134+
{2, Digit, "reprint5ultimatum"},
135+
{4, Empty, "reprintwoolpantryunworried"},
136+
{4, LatinUpper, "reprintXultimatumIunworriedGrecoil"},
137+
{4, FromRangeTable(unicode.P), "reprint」pantry𛲟mummify❩securely"},
138+
{12, String("-"), "reprint-wool-pantry-unworried-mummify-veneering-securely-munchkin-juiciness-steep-cresting-dastardly"},
139+
{12, FromCharset("-~_"), "reprint-ultimatum-unworried~recoil~munchkin~phrase~cubical_haunt-voice-cycle_acetone~grunt"},
140+
{12, ASCIINoLettersNumbers, "reprint,ultimatum+unworried)recoil?munchkin]phrase\"cubical|haunt(voice$cycle/acetone$grunt"},
141+
{12, ASCIINoLetters, "reprint\\ultimatum-unworried+recoil+munchkin%phrase.cubical>haunt6voice$cycle{acetone`grunt"},
142+
} {
143+
tr := newTestRand()
144+
145+
pass, err := RepeatGen(EFFLargeWordlist, tc.sep, tc.count).Password(tr)
146+
if !assert.NoErrorf(t, err, "valid range should not error when generating: %v", tc) {
147+
continue
148+
}
149+
150+
assert.Equal(t, tc.expect, pass, "valid range expected password: %v", tc)
151+
}
152+
}
153+
116154
func TestRandomRepeat(t *testing.T) {
117155
const maxInt = 1<<(bits.UintSize-1) - 1
118156

0 commit comments

Comments
 (0)