initial version

This commit is contained in:
Peter 2023-04-25 15:56:11 +02:00
parent 15b0844a8b
commit 9bf6cb3539
No known key found for this signature in database
8 changed files with 456 additions and 0 deletions

1
.gitignore vendored
View file

@ -21,3 +21,4 @@
# Go workspace file
go.work
.idea/

9
api.go Normal file
View 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
View file

@ -0,0 +1,3 @@
module code.icb4dc0.de/prskr/go-pwgen
go 1.20

91
helpers.go Normal file
View 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
View 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
View 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
View 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
View 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'
}