Add internal randstream and pwgen packages
このコミットが含まれているのは:
@@ -0,0 +1,195 @@
|
|||||||
|
// Package pwgen generates memorable passwords from the system dictionary.
|
||||||
|
// It backs `sutil generate password`.
|
||||||
|
//
|
||||||
|
// A password is three dictionary words, each followed by one decimal
|
||||||
|
// digit, joined by a delimiter that is chosen at random from Delimiters
|
||||||
|
// and used in both positions:
|
||||||
|
//
|
||||||
|
// <word><digit><delim><word><digit><delim><word><digit>
|
||||||
|
//
|
||||||
|
// for example "marble7-sprout2-velvet9".
|
||||||
|
//
|
||||||
|
// Words come from the operating system's word list (/usr/share/dict/words
|
||||||
|
// on macOS and most Unix systems) through github.com/kavfixnel/words,
|
||||||
|
// filtered to words made only of lowercase ASCII letters whose length is
|
||||||
|
// within the configured bounds. That drops proper nouns, abbreviations,
|
||||||
|
// and anything awkward to type. All random choices use crypto/rand.
|
||||||
|
package pwgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/kavfixnel/words"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// WordCount is the number of words in every generated password.
|
||||||
|
WordCount = 3
|
||||||
|
|
||||||
|
// Delimiters is the set of characters from which the delimiter that
|
||||||
|
// separates the words of a password is chosen.
|
||||||
|
Delimiters = ".-_|~+=:,!@#%^&*"
|
||||||
|
|
||||||
|
// DefaultMinWordLength is the shortest word used when
|
||||||
|
// Params.MinWordLength is zero.
|
||||||
|
DefaultMinWordLength = 4
|
||||||
|
|
||||||
|
// DefaultMaxWordLength is the longest word used when
|
||||||
|
// Params.MaxWordLength is zero.
|
||||||
|
DefaultMaxWordLength = 8
|
||||||
|
|
||||||
|
// digitCount is the number of decimal digits (0 through 9) one of
|
||||||
|
// which follows every word.
|
||||||
|
digitCount = 10
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrNoUsableWords is returned by New when the word source yields no
|
||||||
|
// words within the length bounds. On a system without a word list,
|
||||||
|
// github.com/kavfixnel/words returns an empty list rather than an
|
||||||
|
// error, so this is also the error for "no system dictionary".
|
||||||
|
ErrNoUsableWords = errors.New("pwgen: no usable words in word list")
|
||||||
|
|
||||||
|
// ErrInvalidWordLength is returned by New when the word length bounds
|
||||||
|
// are negative or the minimum exceeds the maximum.
|
||||||
|
ErrInvalidWordLength = errors.New("pwgen: invalid word length bounds")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Params configures New.
|
||||||
|
type Params struct {
|
||||||
|
// Words, when non-nil, is used instead of the system dictionary. It
|
||||||
|
// is filtered by the same length and character rules.
|
||||||
|
Words []string
|
||||||
|
|
||||||
|
// MinWordLength is the shortest word to use; zero means
|
||||||
|
// DefaultMinWordLength.
|
||||||
|
MinWordLength int
|
||||||
|
|
||||||
|
// MaxWordLength is the longest word to use; zero means
|
||||||
|
// DefaultMaxWordLength.
|
||||||
|
MaxWordLength int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generator produces passwords from a fixed set of usable words. It is
|
||||||
|
// safe for concurrent use.
|
||||||
|
type Generator struct {
|
||||||
|
words []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// New loads and filters the word source described by p. It returns
|
||||||
|
// ErrNoUsableWords if no words survive filtering, which on a system
|
||||||
|
// without a word list is the normal outcome.
|
||||||
|
func New(p Params) (*Generator, error) {
|
||||||
|
minLength, maxLength := p.MinWordLength, p.MaxWordLength
|
||||||
|
|
||||||
|
if minLength == 0 {
|
||||||
|
minLength = DefaultMinWordLength
|
||||||
|
}
|
||||||
|
|
||||||
|
if maxLength == 0 {
|
||||||
|
maxLength = DefaultMaxWordLength
|
||||||
|
}
|
||||||
|
|
||||||
|
if minLength < 0 || maxLength < minLength {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"%w: min %d, max %d", ErrInvalidWordLength, minLength, maxLength,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
source := p.Words
|
||||||
|
if source == nil {
|
||||||
|
systemWords, err := words.NewWordList(nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("pwgen: reading system word list: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
source = systemWords
|
||||||
|
}
|
||||||
|
|
||||||
|
usable := filterWords(source, minLength, maxLength)
|
||||||
|
if len(usable) == 0 {
|
||||||
|
return nil, ErrNoUsableWords
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Generator{words: usable}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UsableWords returns the number of distinct words the generator chooses
|
||||||
|
// from, which determines the strength of its passwords.
|
||||||
|
func (g *Generator) UsableWords() int {
|
||||||
|
return len(g.words)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate returns a new random password.
|
||||||
|
func (g *Generator) Generate() (string, error) {
|
||||||
|
delimiterIndex, err := randomIndex(len(Delimiters))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
delimiter := Delimiters[delimiterIndex]
|
||||||
|
|
||||||
|
var password strings.Builder
|
||||||
|
|
||||||
|
for i := range WordCount {
|
||||||
|
if i > 0 {
|
||||||
|
password.WriteByte(delimiter)
|
||||||
|
}
|
||||||
|
|
||||||
|
wordIndex, err := randomIndex(len(g.words))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
digit, err := randomIndex(digitCount)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
password.WriteString(g.words[wordIndex])
|
||||||
|
password.WriteString(strconv.Itoa(digit))
|
||||||
|
}
|
||||||
|
|
||||||
|
return password.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterWords returns the entries of source that isUsableWord accepts.
|
||||||
|
func filterWords(source []string, minLength, maxLength int) []string {
|
||||||
|
usable := make([]string, 0, len(source))
|
||||||
|
|
||||||
|
for _, word := range source {
|
||||||
|
if isUsableWord(word, minLength, maxLength) {
|
||||||
|
usable = append(usable, word)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return usable
|
||||||
|
}
|
||||||
|
|
||||||
|
// isUsableWord reports whether word is made only of lowercase ASCII
|
||||||
|
// letters and has a length within the bounds.
|
||||||
|
func isUsableWord(word string, minLength, maxLength int) bool {
|
||||||
|
if len(word) < minLength || len(word) > maxLength {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return !strings.ContainsFunc(word, func(r rune) bool {
|
||||||
|
return r < 'a' || r > 'z'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// randomIndex returns a uniformly random integer in [0, n) from
|
||||||
|
// crypto/rand.
|
||||||
|
func randomIndex(n int) (int, error) {
|
||||||
|
value, err := rand.Int(rand.Reader, big.NewInt(int64(n)))
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("pwgen: reading from crypto/rand: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return int(value.Int64()), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
package pwgen_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"sneak.berlin/go/x/internal/pwgen"
|
||||||
|
)
|
||||||
|
|
||||||
|
const generateRounds = 300
|
||||||
|
|
||||||
|
// testWords mixes usable words with entries the filter must drop:
|
||||||
|
// capitalized, too short, too long, punctuation, digits, and non-ASCII.
|
||||||
|
func testWords() []string {
|
||||||
|
return []string{
|
||||||
|
"apple", "banana", "cherry", "grape", "lemon", "mango", "melon", "peach",
|
||||||
|
"Capital", "ab", "abc", "toolongwordhere", "has-dash", "num3er", "éclair",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func usableTestWords() map[string]bool {
|
||||||
|
return map[string]bool{
|
||||||
|
"apple": true, "banana": true, "cherry": true, "grape": true,
|
||||||
|
"lemon": true, "mango": true, "melon": true, "peach": true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewFiltersWords(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
generator, err := pwgen.New(pwgen.Params{Words: testWords()})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, len(usableTestWords()), generator.UsableWords())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewHonorsLengthBounds(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
generator, err := pwgen.New(pwgen.Params{
|
||||||
|
Words: testWords(),
|
||||||
|
MinWordLength: 2,
|
||||||
|
MaxWordLength: 3,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, 2, generator.UsableWords(), `only "ab" and "abc" fit`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewRejectsNoUsableWords(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
_, err := pwgen.New(pwgen.Params{Words: []string{"Capital", "ab"}})
|
||||||
|
require.ErrorIs(t, err, pwgen.ErrNoUsableWords)
|
||||||
|
|
||||||
|
_, err = pwgen.New(pwgen.Params{Words: []string{}})
|
||||||
|
require.ErrorIs(t, err, pwgen.ErrNoUsableWords)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewRejectsInvalidBounds(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
_, err := pwgen.New(pwgen.Params{
|
||||||
|
Words: testWords(),
|
||||||
|
MinWordLength: 9,
|
||||||
|
MaxWordLength: 4,
|
||||||
|
})
|
||||||
|
require.ErrorIs(t, err, pwgen.ErrInvalidWordLength)
|
||||||
|
|
||||||
|
_, err = pwgen.New(pwgen.Params{Words: testWords(), MinWordLength: -1})
|
||||||
|
require.ErrorIs(t, err, pwgen.ErrInvalidWordLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateFormat(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
generator, err := pwgen.New(pwgen.Params{Words: testWords()})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
usable := usableTestWords()
|
||||||
|
|
||||||
|
for range generateRounds {
|
||||||
|
password, err := generator.Generate()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
words, digits, delimiter := parsePassword(t, password)
|
||||||
|
|
||||||
|
assert.Contains(t, pwgen.Delimiters, string(delimiter))
|
||||||
|
assert.Len(t, words, pwgen.WordCount)
|
||||||
|
|
||||||
|
for _, word := range words {
|
||||||
|
assert.True(t, usable[word], "unexpected word %q in %q", word, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, digit := range digits {
|
||||||
|
assert.True(t, digit >= '0' && digit <= '9', "digit in %q", password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateVariesDelimitersAndDigits(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
generator, err := pwgen.New(pwgen.Params{Words: testWords()})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
delimitersSeen := map[byte]bool{}
|
||||||
|
digitsSeen := map[byte]bool{}
|
||||||
|
passwordsSeen := map[string]bool{}
|
||||||
|
|
||||||
|
for range generateRounds {
|
||||||
|
password, err := generator.Generate()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, digits, delimiter := parsePassword(t, password)
|
||||||
|
delimitersSeen[delimiter] = true
|
||||||
|
passwordsSeen[password] = true
|
||||||
|
|
||||||
|
for _, digit := range digits {
|
||||||
|
digitsSeen[digit] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Greater(t, len(delimitersSeen), 1, "delimiter should vary")
|
||||||
|
assert.Greater(t, len(digitsSeen), 1, "digits should vary")
|
||||||
|
assert.Greater(t, len(passwordsSeen), generateRounds/2, "passwords repeat")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewWithSystemDictionary(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
generator, err := pwgen.New(pwgen.Params{})
|
||||||
|
if errors.Is(err, pwgen.ErrNoUsableWords) {
|
||||||
|
t.Skip("this system has no word list")
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Greater(t, generator.UsableWords(), 1000)
|
||||||
|
|
||||||
|
password, err := generator.Generate()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
parsePassword(t, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsePassword splits a password of the form
|
||||||
|
// <word><digit><delim><word><digit><delim><word><digit> into its words,
|
||||||
|
// digits, and the delimiter, failing the test if it is not of that shape.
|
||||||
|
func parsePassword(t *testing.T, password string) ([]string, []byte, byte) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
delimiterIndex := strings.IndexFunc(password, func(r rune) bool {
|
||||||
|
return (r < 'a' || r > 'z') && (r < '0' || r > '9')
|
||||||
|
})
|
||||||
|
require.NotEqual(t, -1, delimiterIndex, "no delimiter in %q", password)
|
||||||
|
|
||||||
|
delimiter := password[delimiterIndex]
|
||||||
|
parts := strings.Split(password, string(delimiter))
|
||||||
|
require.Len(t, parts, pwgen.WordCount, "wrong shape: %q", password)
|
||||||
|
|
||||||
|
words := make([]string, 0, len(parts))
|
||||||
|
digits := make([]byte, 0, len(parts))
|
||||||
|
|
||||||
|
for _, part := range parts {
|
||||||
|
require.GreaterOrEqual(t, len(part), 2, "short part in %q", password)
|
||||||
|
|
||||||
|
words = append(words, part[:len(part)-1])
|
||||||
|
digits = append(digits, part[len(part)-1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return words, digits, delimiter
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
// Package randstream provides a fast, unbounded stream of pseudorandom
|
||||||
|
// bytes as an io.Reader. It backs `sutil generate random`.
|
||||||
|
//
|
||||||
|
// It exists because reading /dev/urandom is slow: operating system random
|
||||||
|
// devices are built for small requests of key material, not for filling
|
||||||
|
// disks or producing gigabytes of test data. A Reader instead expands a
|
||||||
|
// 32-byte key with the ChaCha8 generator from math/rand/v2, which runs at
|
||||||
|
// close to memory bandwidth and never blocks.
|
||||||
|
//
|
||||||
|
// When Params.Seed is set, the key is SHA-256 of the seed string and the
|
||||||
|
// stream is fully deterministic: the same seed always produces the same
|
||||||
|
// bytes, which makes test data reproducible. When Seed is empty, the key
|
||||||
|
// is read from crypto/rand (the operating system's secure random source,
|
||||||
|
// /dev/urandom or its equivalent) and the stream is unpredictable.
|
||||||
|
//
|
||||||
|
// The output is not meant to be used as cryptographic key material; use
|
||||||
|
// crypto/rand for that.
|
||||||
|
package randstream
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
mathrand "math/rand/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// KeySize is the size in bytes of the key that seeds the stream.
|
||||||
|
const KeySize = 32
|
||||||
|
|
||||||
|
// Params configures New.
|
||||||
|
type Params struct {
|
||||||
|
// Seed, when non-empty, makes the stream deterministic: the same seed
|
||||||
|
// always yields the same byte sequence. When empty, the stream is
|
||||||
|
// keyed from crypto/rand and differs on every call to New.
|
||||||
|
Seed string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reader is an unbounded stream of pseudorandom bytes. Read never returns
|
||||||
|
// an error and never returns fewer bytes than requested. A Reader is not
|
||||||
|
// safe for concurrent use.
|
||||||
|
type Reader struct {
|
||||||
|
generator *mathrand.ChaCha8
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a Reader configured by p.
|
||||||
|
func New(p Params) (*Reader, error) {
|
||||||
|
key, err := keyFromParams(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Reader{generator: mathrand.NewChaCha8(key)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read fills b with pseudorandom bytes. It always returns len(b) and a nil
|
||||||
|
// error.
|
||||||
|
func (r *Reader) Read(b []byte) (int, error) {
|
||||||
|
return r.generator.Read(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// keyFromParams derives the ChaCha8 key: SHA-256 of the seed string, or
|
||||||
|
// fresh bytes from crypto/rand when no seed is given.
|
||||||
|
func keyFromParams(p Params) ([KeySize]byte, error) {
|
||||||
|
if p.Seed != "" {
|
||||||
|
return sha256.Sum256([]byte(p.Seed)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var key [KeySize]byte
|
||||||
|
|
||||||
|
_, err := rand.Read(key[:])
|
||||||
|
if err != nil {
|
||||||
|
return key, fmt.Errorf("randstream: reading key from crypto/rand: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package randstream_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"sneak.berlin/go/x/internal/randstream"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
sampleSize = 1 << 20
|
||||||
|
helloSeed = "hello"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSeededStreamIsDeterministic(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
first := readSample(t, randstream.Params{Seed: helloSeed}, sampleSize)
|
||||||
|
second := readSample(t, randstream.Params{Seed: helloSeed}, sampleSize)
|
||||||
|
|
||||||
|
assert.Equal(t, first, second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDifferentSeedsDiffer(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
hello := readSample(t, randstream.Params{Seed: helloSeed}, sampleSize)
|
||||||
|
world := readSample(t, randstream.Params{Seed: "world"}, sampleSize)
|
||||||
|
|
||||||
|
assert.NotEqual(t, hello, world)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnseededStreamsDiffer(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
first := readSample(t, randstream.Params{Seed: ""}, sampleSize)
|
||||||
|
second := readSample(t, randstream.Params{Seed: ""}, sampleSize)
|
||||||
|
|
||||||
|
assert.NotEqual(t, first, second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadAlwaysFillsBuffer(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
stream, err := randstream.New(randstream.Params{Seed: "fill"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for _, size := range []int{0, 1, 7, 64, 65, 4096, sampleSize} {
|
||||||
|
buffer := make([]byte, size)
|
||||||
|
|
||||||
|
n, err := stream.Read(buffer)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, size, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStreamIsContinuousAcrossReads(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const headSize, tailSize = 300, 700
|
||||||
|
|
||||||
|
whole := readSample(t, randstream.Params{Seed: "continuous"}, headSize+tailSize)
|
||||||
|
|
||||||
|
stream, err := randstream.New(randstream.Params{Seed: "continuous"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
head := make([]byte, headSize)
|
||||||
|
tail := make([]byte, tailSize)
|
||||||
|
|
||||||
|
_, err = io.ReadFull(stream, head)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = io.ReadFull(stream, tail)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, whole[:headSize], head)
|
||||||
|
assert.Equal(t, whole[headSize:], tail)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOutputIsNotTriviallyPatterned(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
sample := readSample(t, randstream.Params{Seed: "entropy"}, sampleSize)
|
||||||
|
|
||||||
|
var seen [256]bool
|
||||||
|
for _, b := range sample {
|
||||||
|
seen[b] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for value, wasSeen := range seen {
|
||||||
|
assert.True(t, wasSeen, "byte value %d never appeared", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// readSample reads size bytes from a new Reader configured by p.
|
||||||
|
func readSample(t *testing.T, p randstream.Params, size int) []byte {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
stream, err := randstream.New(p)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
sample := make([]byte, size)
|
||||||
|
|
||||||
|
_, err = io.ReadFull(stream, sample)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return sample
|
||||||
|
}
|
||||||
新しいイシューから参照
ユーザーをブロックする