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.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