initial version
This commit is contained in:
parent
15b0844a8b
commit
9bf6cb3539
8 changed files with 456 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -21,3 +21,4 @@
|
||||||
# Go workspace file
|
# Go workspace file
|
||||||
go.work
|
go.work
|
||||||
|
|
||||||
|
.idea/
|
||||||
|
|
9
api.go
Normal file
9
api.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package pwgen
|
||||||
|
|
||||||
|
type GeneratorOption interface {
|
||||||
|
ApplyToOptions(options *options)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Generator interface {
|
||||||
|
Generate(opts ...GeneratorOption) (string, error)
|
||||||
|
}
|
3
go.mod
Normal file
3
go.mod
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
module code.icb4dc0.de/prskr/go-pwgen
|
||||||
|
|
||||||
|
go 1.20
|
91
helpers.go
Normal file
91
helpers.go
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
package pwgen
|
||||||
|
|
||||||
|
func runeFor(alphabet []rune, prng Int32n) rune {
|
||||||
|
return alphabet[prng.Int31n(int32(len(alphabet)))]
|
||||||
|
}
|
||||||
|
|
||||||
|
func alphabet(start, end rune) (result []rune) {
|
||||||
|
length := end - start + 1
|
||||||
|
result = make([]rune, length)
|
||||||
|
for i := int32(0); i < length; i++ {
|
||||||
|
result[i] = start + i
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func merge[T any](slices ...[]T) []T {
|
||||||
|
var totalLength int
|
||||||
|
for i := range slices {
|
||||||
|
totalLength += len(slices[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]T, totalLength)
|
||||||
|
|
||||||
|
var offset int
|
||||||
|
for i := range slices {
|
||||||
|
copy(result[offset:], slices[i])
|
||||||
|
offset += len(slices[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func shuffle[T comparable](in []T, prng Int32n) (result []T) {
|
||||||
|
if !canShuffle(in) {
|
||||||
|
return in
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
result = make([]T, 0, len(in))
|
||||||
|
temp := make([]T, len(in))
|
||||||
|
copy(temp, in)
|
||||||
|
|
||||||
|
for len(temp) > 0 {
|
||||||
|
var i int32
|
||||||
|
for {
|
||||||
|
i = prng.Int31n(int32(len(temp)))
|
||||||
|
if i != int32(len(result)) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = append(result, temp[i])
|
||||||
|
copy(temp[i:], temp[i+1:])
|
||||||
|
temp = temp[:len(temp)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if !sequenceEqual(in, result) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func canShuffle[T comparable](in []T) bool {
|
||||||
|
if len(in) < 2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
uniq := make(map[T]bool)
|
||||||
|
for _, r := range in {
|
||||||
|
uniq[r] = true
|
||||||
|
if len(uniq) > 1 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func sequenceEqual[T comparable](first, second []T) bool {
|
||||||
|
if len(first) != len(second) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range first {
|
||||||
|
if first[i] != second[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
84
helpers_test.go
Normal file
84
helpers_test.go
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
package pwgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_alphabet(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
start rune
|
||||||
|
end rune
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantResult []rune
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Lowercase runes",
|
||||||
|
args: args{
|
||||||
|
start: 'a',
|
||||||
|
end: 'z',
|
||||||
|
},
|
||||||
|
//lowercase runes a to z
|
||||||
|
wantResult: []rune{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Uppercase runes",
|
||||||
|
args: args{
|
||||||
|
start: 'A',
|
||||||
|
end: 'Z',
|
||||||
|
},
|
||||||
|
wantResult: []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Digit runes",
|
||||||
|
args: args{
|
||||||
|
start: '0',
|
||||||
|
end: '9',
|
||||||
|
},
|
||||||
|
wantResult: []rune("0123456789"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if gotResult := alphabet(tt.args.start, tt.args.end); !reflect.DeepEqual(gotResult, tt.wantResult) {
|
||||||
|
t.Errorf("alphabet() = %v, want %v", gotResult, tt.wantResult)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Fuzz_shuffle(f *testing.F) {
|
||||||
|
defaultPrng := rand.New(rand.NewSource(1))
|
||||||
|
|
||||||
|
f.Add("hello, world")
|
||||||
|
f.Add("a")
|
||||||
|
f.Add("aa")
|
||||||
|
f.Add("abcdef")
|
||||||
|
f.Add("01234")
|
||||||
|
|
||||||
|
f.Fuzz(func(t *testing.T, input string) {
|
||||||
|
in := []rune(input)
|
||||||
|
out := shuffle(in, defaultPrng)
|
||||||
|
|
||||||
|
if sliceReferenceEqual(in, out) {
|
||||||
|
t.Logf("returned the same input")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(input) > 1 && string(out) == input {
|
||||||
|
t.Errorf("shuffle() = %s, input %s", string(out), input)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func sliceReferenceEqual[T any](a, b []T) bool {
|
||||||
|
ah := (*reflect.SliceHeader)(unsafe.Pointer(&a))
|
||||||
|
bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
|
||||||
|
|
||||||
|
return ah.Data == bh.Data
|
||||||
|
}
|
126
options.go
Normal file
126
options.go
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
package pwgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultLength = 20
|
||||||
|
DefaultSpecialsAlphabet = `!?~@#$%^&*()-+={}[]\/{}|<>`
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrLengthOverflow = errors.New("length overflow")
|
||||||
|
|
||||||
|
type generatorOptionFunc func(options *options)
|
||||||
|
|
||||||
|
func (f generatorOptionFunc) ApplyToOptions(options *options) {
|
||||||
|
f(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithLength(length uint) GeneratorOption {
|
||||||
|
return generatorOptionFunc(func(options *options) {
|
||||||
|
options.Length = length
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithLetters(letters uint) GeneratorOption {
|
||||||
|
return generatorOptionFunc(func(options *options) {
|
||||||
|
options.Letters = letters
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithUppercase(uppercase uint) GeneratorOption {
|
||||||
|
return generatorOptionFunc(func(options *options) {
|
||||||
|
options.Uppercase = uppercase
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithLowercase(lowercase uint) GeneratorOption {
|
||||||
|
return generatorOptionFunc(func(options *options) {
|
||||||
|
options.Lowercase = lowercase
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithDigits(digits uint) GeneratorOption {
|
||||||
|
return generatorOptionFunc(func(options *options) {
|
||||||
|
options.Digits = digits
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithSpecials(specials uint) GeneratorOption {
|
||||||
|
return generatorOptionFunc(func(options *options) {
|
||||||
|
options.Specials = specials
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithSpecialsAlphabet(specialsAlphabet string) GeneratorOption {
|
||||||
|
return generatorOptionFunc(func(options *options) {
|
||||||
|
if specialsAlphabet == "" {
|
||||||
|
specialsAlphabet = DefaultSpecialsAlphabet
|
||||||
|
}
|
||||||
|
options.SpecialsAlphabet = specialsAlphabet
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPRNG(prng Int32n) GeneratorOption {
|
||||||
|
return generatorOptionFunc(func(options *options) {
|
||||||
|
options.PRNG = prng
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultOptions(opts ...GeneratorOption) *options {
|
||||||
|
o := &options{
|
||||||
|
PRNG: rand.New(rand.NewSource(time.Now().Unix())),
|
||||||
|
Length: DefaultLength,
|
||||||
|
Letters: 0,
|
||||||
|
Digits: 3,
|
||||||
|
Uppercase: 3,
|
||||||
|
Lowercase: 3,
|
||||||
|
Specials: 3,
|
||||||
|
SpecialsAlphabet: DefaultSpecialsAlphabet,
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range opts {
|
||||||
|
opts[i].ApplyToOptions(o)
|
||||||
|
}
|
||||||
|
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
|
type Int32n interface {
|
||||||
|
Int31n(n int32) int32
|
||||||
|
}
|
||||||
|
|
||||||
|
type options struct {
|
||||||
|
PRNG Int32n
|
||||||
|
Length uint
|
||||||
|
Letters uint
|
||||||
|
Digits uint
|
||||||
|
Uppercase uint
|
||||||
|
Lowercase uint
|
||||||
|
Specials uint
|
||||||
|
SpecialsAlphabet string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o options) effectiveLength() (uint, error) {
|
||||||
|
var length uint
|
||||||
|
|
||||||
|
for _, i := range []uint{o.Digits, o.Lowercase, o.Uppercase, o.Specials} {
|
||||||
|
if length+i < length {
|
||||||
|
return 0, ErrLengthOverflow
|
||||||
|
}
|
||||||
|
length += i
|
||||||
|
}
|
||||||
|
|
||||||
|
if upperAndLower := o.Lowercase + o.Uppercase; upperAndLower < o.Letters {
|
||||||
|
length += o.Letters - upperAndLower
|
||||||
|
}
|
||||||
|
|
||||||
|
if length > o.Length {
|
||||||
|
return length, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return o.Length, nil
|
||||||
|
}
|
51
pwgen.go
Normal file
51
pwgen.go
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
package pwgen
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ Generator = (*DefaultGenerator)(nil)
|
||||||
|
Default DefaultGenerator
|
||||||
|
lowerCase = alphabet('a', 'z')
|
||||||
|
upperCase = alphabet('A', 'Z')
|
||||||
|
letters = merge(lowerCase, upperCase)
|
||||||
|
digits = alphabet('0', '9')
|
||||||
|
)
|
||||||
|
|
||||||
|
type DefaultGenerator struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DefaultGenerator) Generate(opts ...GeneratorOption) (string, error) {
|
||||||
|
compiledOptions := defaultOptions(opts...)
|
||||||
|
fullAlphabet := merge(lowerCase, upperCase, digits, []rune(compiledOptions.SpecialsAlphabet))
|
||||||
|
|
||||||
|
effectiveLength, err := compiledOptions.effectiveLength()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
generated := make([]rune, 0, effectiveLength)
|
||||||
|
|
||||||
|
for i := uint(0); i < compiledOptions.Lowercase; i++ {
|
||||||
|
generated = append(generated, runeFor(lowerCase, compiledOptions.PRNG))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := uint(0); i < compiledOptions.Uppercase; i++ {
|
||||||
|
generated = append(generated, runeFor(upperCase, compiledOptions.PRNG))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := uint(0); i < compiledOptions.Digits; i++ {
|
||||||
|
generated = append(generated, runeFor(digits, compiledOptions.PRNG))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := uint(0); i < compiledOptions.Specials; i++ {
|
||||||
|
generated = append(generated, runeFor([]rune(compiledOptions.SpecialsAlphabet), compiledOptions.PRNG))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := compiledOptions.Lowercase + compiledOptions.Uppercase; i < compiledOptions.Letters; i++ {
|
||||||
|
generated = append(generated, runeFor(letters, compiledOptions.PRNG))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := uint(len(generated)); i < effectiveLength; i++ {
|
||||||
|
generated = append(generated, runeFor(fullAlphabet, compiledOptions.PRNG))
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(shuffle(generated, compiledOptions.PRNG)), nil
|
||||||
|
}
|
91
pwgen_test.go
Normal file
91
pwgen_test.go
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
package pwgen_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.icb4dc0.de/prskr/go-pwgen"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FuzzDefaultGenerator_Generate(f *testing.F) {
|
||||||
|
|
||||||
|
f.Add(uint(0), uint(2), uint(2), uint(2), uint(2))
|
||||||
|
f.Add(uint(5), uint(2), uint(2), uint(2), uint(2))
|
||||||
|
f.Add(uint(3), uint(2), uint(2), uint(2), uint(2))
|
||||||
|
f.Add(uint(3), uint(2), uint(2), uint(2), uint(0))
|
||||||
|
f.Add(uint(3), uint(2), uint(2), uint(2), uint(5))
|
||||||
|
f.Add(uint(10), uint(2), uint(2), uint(2), uint(5))
|
||||||
|
|
||||||
|
f.Fuzz(func(t *testing.T, letters, digits, uppercase, lowercase, specials uint) {
|
||||||
|
password, err := pwgen.Default.Generate(
|
||||||
|
pwgen.WithLetters(letters),
|
||||||
|
pwgen.WithDigits(digits),
|
||||||
|
pwgen.WithUppercase(uppercase),
|
||||||
|
pwgen.WithLowercase(lowercase),
|
||||||
|
pwgen.WithSpecials(specials),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pwgen.ErrLengthOverflow) {
|
||||||
|
t.Logf("pwgen.ErrLengthOverflow")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if matchesCriteria[rune]([]rune(password), containsDigit) < digits {
|
||||||
|
t.Errorf("password %q does not contain %d digits", password, digits)
|
||||||
|
}
|
||||||
|
|
||||||
|
if matchesCriteria[rune]([]rune(password), containsLetter) < letters {
|
||||||
|
t.Errorf("password %q does not contain %d letters", password, letters)
|
||||||
|
}
|
||||||
|
|
||||||
|
if matchesCriteria[rune]([]rune(password), containsUppercase) < uppercase {
|
||||||
|
t.Errorf("password %q does not contain %d uppercase", password, uppercase)
|
||||||
|
}
|
||||||
|
|
||||||
|
if matchesCriteria[rune]([]rune(password), containsLowercase) < lowercase {
|
||||||
|
t.Errorf("password %q does not contain %d lowercase", password, lowercase)
|
||||||
|
}
|
||||||
|
|
||||||
|
if matchesCriteria[rune]([]rune(password), containsSpecials) < specials {
|
||||||
|
t.Errorf("password %q does not contain %d specials", password, specials)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchesCriteria[T any](s []T, criteria func(T) bool) uint {
|
||||||
|
var found uint
|
||||||
|
for i := range s {
|
||||||
|
if criteria(s[i]) {
|
||||||
|
found += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsLetter(r rune) bool {
|
||||||
|
return r >= 'A' && r <= 'z'
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsDigit(r rune) bool {
|
||||||
|
return r >= '0' && r <= '9'
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsSpecials(r rune) bool {
|
||||||
|
for i := range pwgen.DefaultSpecialsAlphabet {
|
||||||
|
if r == rune(pwgen.DefaultSpecialsAlphabet[i]) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsLowercase(r rune) bool {
|
||||||
|
return r >= 'a' && r <= 'z'
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsUppercase(r rune) bool {
|
||||||
|
return r >= 'A' && r <= 'Z'
|
||||||
|
}
|
Loading…
Reference in a new issue