feat(rands): initial implementation

This commit is contained in:
2021-01-20 02:57:34 +00:00
parent 3ddcfd415c
commit 597fe535d3
18 changed files with 1603 additions and 1 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
/*.tidy-check
/bin/*
/coverage.out
/output.txt

78
.golangci.yml Normal file
View File

@@ -0,0 +1,78 @@
linters-settings:
funlen:
lines: 100
statements: 150
gocyclo:
min-complexity: 20
golint:
min-confidence: 0
govet:
check-shadowing: true
enable-all: true
lll:
line-length: 80
tab-width: 4
maligned:
suggest-new: true
misspell:
locale: US
linters:
disable-all: true
enable:
- bodyclose
- deadcode
- depguard
- dupl
- errcheck
- funlen
- gochecknoinits
- goconst
- gocritic
- gocyclo
- goerr113
- goimports
- golint
- goprintffuncname
- gosec
- gosimple
- govet
- ineffassign
- lll
- misspell
- nakedret
- nlreturn
- noctx
- nolintlint
- scopelint
- sqlclosecheck
- staticcheck
- structcheck
- typecheck
- unconvert
- unused
- varcheck
- whitespace
issues:
include:
# - EXC0002 # disable excluding of issues about comments from golint
exclude:
- Using the variable on range scope `tt` in function literal
- Using the variable on range scope `tc` in function literal
exclude-rules:
- path: "_test\\.go"
linters:
- funlen
- dupl
- source: "^//go:generate "
linters:
- lll
- source: "`json:"
linters:
- lll
run:
timeout: 2m
allow-parallel-runners: true
modules-download-mode: readonly

20
LICENSE Normal file
View File

@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2021 Jim Myhrberg
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

173
Makefile Normal file
View File

@@ -0,0 +1,173 @@
GOMODNAME := $(shell grep 'module' go.mod | sed -e 's/^module //')
SOURCES := $(shell find . -name "*.go" -or -name "go.mod" -or -name "go.sum" \
-or -name "Makefile")
# Verbose output
ifdef VERBOSE
V = -v
endif
#
# Environment
#
BINDIR := bin
TOOLDIR := $(BINDIR)/tools
# Global environment variables for all targets
SHELL ?= /bin/bash
SHELL := env \
GO111MODULE=on \
GOBIN=$(CURDIR)/$(TOOLDIR) \
CGO_ENABLED=1 \
PATH='$(CURDIR)/$(BINDIR):$(CURDIR)/$(TOOLDIR):$(PATH)' \
$(SHELL)
#
# Defaults
#
# Default target
.DEFAULT_GOAL := test
#
# Tools
#
TOOLS += $(TOOLDIR)/gobin
gobin: $(TOOLDIR)/gobin
$(TOOLDIR)/gobin:
GO111MODULE=off go get -u github.com/myitcv/gobin
# external tool
define tool # 1: binary-name, 2: go-import-path
TOOLS += $(TOOLDIR)/$(1)
.PHONY: $(1)
$(1): $(TOOLDIR)/$(1)
$(TOOLDIR)/$(1): $(TOOLDIR)/gobin Makefile
gobin $(V) "$(2)"
endef
$(eval $(call tool,godoc,golang.org/x/tools/cmd/godoc))
$(eval $(call tool,gofumports,mvdan.cc/gofumpt/gofumports))
$(eval $(call tool,golangci-lint,github.com/golangci/golangci-lint/cmd/golangci-lint@v1.35))
$(eval $(call tool,gomod,github.com/Helcaraxan/gomod))
.PHONY: tools
tools: $(TOOLS)
#
# Development
#
TEST ?= $$(go list ./... | grep -v 'vendor')
BENCH ?= .
.PHONY: clean
clean:
rm -f $(TOOLS)
rm -f ./coverage.out ./go.mod.tidy-check ./go.sum.tidy-check
.PHONY: test
test:
go test $(V) -count=1 $(TESTARGS) $(TEST)
.PHONY: test-deps
test-deps:
go test all
.PHONY: lint
lint: golangci-lint
GOGC=off golangci-lint $(V) run
.PHONY: format
format: gofumports
gofumports -w .
.SILENT: bench
.PHONY: bench
bench:
go test $(V) -count=1 -bench=$(BENCH) $(TESTARGS) $(TEST)
#
# Coverage
#
.PHONY: cov
cov: coverage.out
.PHONY: cov-html
cov-html: coverage.out
go tool cover -html=./coverage.out
.PHONY: cov-func
cov-func: coverage.out
go tool cover -func=./coverage.out
coverage.out: $(SOURCES)
go test $(V) -covermode=count -coverprofile=./coverage.out ./...
#
# Dependencies
#
.PHONY: deps
deps:
$(info Downloading dependencies)
go mod download
.PHONY: deps-update
deps-update:
$(info Downloading dependencies)
go get -u ./...
.PHONY: deps-analyze
deps-analyze: gomod
gomod analyze
.PHONY: tidy
tidy:
go mod tidy $(V)
.PHONY: verify
verify:
go mod verify
.SILENT: check-tidy
.PHONY: check-tidy
check-tidy:
cp go.mod go.mod.tidy-check
cp go.sum go.sum.tidy-check
go mod tidy
( \
diff go.mod go.mod.tidy-check && \
diff go.sum go.sum.tidy-check && \
rm -f go.mod go.sum && \
mv go.mod.tidy-check go.mod && \
mv go.sum.tidy-check go.sum \
) || ( \
rm -f go.mod go.sum && \
mv go.mod.tidy-check go.mod && \
mv go.sum.tidy-check go.sum; \
exit 1 \
)
#
# Documentation
#
# Serve docs
.PHONY: docs
docs: godoc
$(info serviing docs on http://127.0.0.1:6060/pkg/$(GOMODNAME)/)
@godoc -http=127.0.0.1:6060
#
# Release
#
.PHONY: new-version
new-version:
npx standard-version

View File

@@ -1 +1,81 @@
# rands <h1 align="center">
rands
</h1>
<p align="center">
<strong>
Go package providing a suite of functions that use `crypto/rand` to generate
cryptographically secure random strings in various formats, as well as ints
and bytes.
</strong>
</p>
<p align="center">
<a href="https://pkg.go.dev/github.com/jimeh/rands">
<img src="https://img.shields.io/badge/%E2%80%8B-reference-387b97.svg?logo=go&logoColor=white"
alt="Go Reference">
</a>
<a href="https://github.com/jimeh/rands/releases">
<img src="https://img.shields.io/github/v/tag/jimeh/rands?label=release" alt="GitHub tag (latest SemVer)">
</a>
<a href="https://github.com/jimeh/rands/actions">
<img src="https://img.shields.io/github/workflow/status/jimeh/rands/CI.svg?logo=github" alt="Actions Status">
</a>
<a href="https://codeclimate.com/github/jimeh/rands">
<img src="https://img.shields.io/codeclimate/coverage/jimeh/rands.svg?logo=code%20climate" alt="Coverage">
</a>
<a href="https://github.com/jimeh/rands/issues">
<img src="https://img.shields.io/github/issues-raw/jimeh/rands.svg?style=flat&logo=github&logoColor=white"
alt="GitHub issues">
</a>
<a href="https://github.com/jimeh/rands/pulls">
<img src="https://img.shields.io/github/issues-pr-raw/jimeh/rands.svg?style=flat&logo=github&logoColor=white" alt="GitHub pull requests">
</a>
<a href="https://github.com/jimeh/rands/blob/master/LICENSE">
<img src="https://img.shields.io/github/license/jimeh/rands.svg?style=flat" alt="License Status">
</a>
</p>
```go
s, _ := rands.Base64(16) // => CYxqEdUB1Rzno3SyZu2g/g==
s, _ := rands.Base64URL(16) // => zlqw9aFqcFggbk2asn3_aQ
s, _ := rands.Hex(16) // => 956e2ec9e7f19ddd58bb935826926531
s, _ := rands.Alphanumeric(16) // => Fvk1PkrmG5crgOjT
s, _ := rands.Alphabetic(16) // => XEJIzcZufHkuUmRM
s, _ := rands.Upper(16) // => UMAGAFPPNDRGLUPZ
s, _ := rands.UpperNumeric(16) // => DF0CQS0TK9CPUO3E
s, _ := rands.Lower(16) // => ocsmggykzrxzfwgt
s, _ := rands.LowerNumeric(16) // => rwlv7a1p7klqffs5
s, _ := rands.Numeric(16) // => 9403373143598295
s, _ := rands.String(16, "abcdefABCDEF") // => adCDCaDEdeffeDeb
s, _ := rands.UnicodeString(16, []rune("九七二人入八力十下三千上口土夕大")) // => 下下口九力下土夕下土八上二夕大三
s, _ := rands.DNSLabel(16) // => z0ij9o8qkbs0ru-h
n, _ := rands.Int(2147483647) // => 1334400235
n, _ := rands.Int64(int64(9223372036854775807)) // => 8256935979116161233
b, _ := rands.Bytes(8) // => [0 220 137 243 135 204 34 63]
```
## Import
```
import "github.com/jimeh/rands"
```
## Documentation
Please see the
[Go Reference](https://pkg.go.dev/github.com/jimeh/rands#section-documentation)
for documentation and examples.
## Benchmarks
Benchmark reports and graphs are available here:
https://jimeh.me/rands/dev/bench/
## License
[MIT](https://github.com/jimeh/rands/blob/master/LICENSE)

14
bytes.go Normal file
View File

@@ -0,0 +1,14 @@
package rands
import "crypto/rand"
// Bytes generates a byte slice of n number of random bytes.
func Bytes(n int) ([]byte, error) {
b := make([]byte, n)
_, err := rand.Read(b)
if err != nil {
return nil, err
}
return b, nil
}

12
bytes_example_test.go Normal file
View File

@@ -0,0 +1,12 @@
package rands_test
import (
"fmt"
"github.com/jimeh/rands"
)
func ExampleBytes() {
b, _ := rands.Bytes(8)
fmt.Printf("%+v\n", b) // => [0 220 137 243 135 204 34 63]
}

27
bytes_test.go Normal file
View File

@@ -0,0 +1,27 @@
package rands
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestBytes(t *testing.T) {
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
got, _ := Bytes(tt.n)
assert.Len(t, got, tt.n)
})
}
}
func BenchmarkBytes(b *testing.B) {
for _, tt := range testCases {
b.Run(tt.name, func(b *testing.B) {
for n := 0; n < b.N; n++ {
_, _ = Bytes(tt.n)
}
})
}
}

5
go.mod Normal file
View File

@@ -0,0 +1,5 @@
module github.com/jimeh/rands
go 1.15
require github.com/stretchr/testify v1.7.0

11
go.sum Normal file
View File

@@ -0,0 +1,11 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

37
ints.go Normal file
View File

@@ -0,0 +1,37 @@
package rands
import (
"crypto/rand"
"fmt"
"math/big"
)
var errInvalidMaxInt = fmt.Errorf("%w: max cannot be less than 1", errBase)
// Int generates a random int ranging between 0 and max.
func Int(max int) (int, error) {
if max < 1 {
return 0, errInvalidMaxInt
}
r, err := rand.Int(rand.Reader, big.NewInt(int64(max)))
if err != nil {
return 0, err
}
return int(r.Int64()), nil
}
// Int64 generates a random int64 ranging between 0 and max.
func Int64(max int64) (int64, error) {
if max < 1 {
return 0, errInvalidMaxInt
}
r, err := rand.Int(rand.Reader, big.NewInt(max))
if err != nil {
return 0, err
}
return r.Int64(), nil
}

17
ints_example_test.go Normal file
View File

@@ -0,0 +1,17 @@
package rands_test
import (
"fmt"
"github.com/jimeh/rands"
)
func ExampleInt() {
n, _ := rands.Int(2147483647)
fmt.Printf("%d\n", n) // => 1334400235
}
func ExampleInt64() {
n, _ := rands.Int64(int64(9223372036854775807))
fmt.Printf("%d\n", n) // => 8256935979116161233
}

154
ints_test.go Normal file
View File

@@ -0,0 +1,154 @@
package rands
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
)
var testIntCases = []struct {
name string
max int
errIs error
errStr string
}{
{
name: "n=-2394345",
max: -2394345,
errIs: errInvalidMaxInt,
errStr: "rands: max cannot be less than 1",
},
{
name: "n=-409600",
max: -409600,
errIs: errInvalidMaxInt,
errStr: "rands: max cannot be less than 1",
},
{
name: "n=-1024",
max: -1024,
errIs: errInvalidMaxInt,
errStr: "rands: max cannot be less than 1",
},
{
name: "n=-128",
max: -128,
errIs: errInvalidMaxInt,
errStr: "rands: max cannot be less than 1",
},
{
name: "n=-32",
max: -32,
errIs: errInvalidMaxInt,
errStr: "rands: max cannot be less than 1",
},
{
name: "n=-16",
max: -16,
errIs: errInvalidMaxInt,
errStr: "rands: max cannot be less than 1",
},
{
name: "n=-8",
max: -8,
errIs: errInvalidMaxInt,
errStr: "rands: max cannot be less than 1",
},
{
name: "n=-7",
max: -7,
errIs: errInvalidMaxInt,
errStr: "rands: max cannot be less than 1",
},
{
name: "n=-2",
max: -2,
errIs: errInvalidMaxInt,
errStr: "rands: max cannot be less than 1",
},
{
name: "n=-1",
max: -1,
errIs: errInvalidMaxInt,
errStr: "rands: max cannot be less than 1",
},
{
name: "n=0",
max: 0,
errIs: errInvalidMaxInt,
errStr: "rands: max cannot be less than 1",
},
{name: "n=1", max: 1},
{name: "n=2", max: 2},
{name: "n=7", max: 7},
{name: "n=8", max: 8},
{name: "n=16", max: 16},
{name: "n=32", max: 32},
{name: "n=128", max: 128},
{name: "n=1024", max: 1024},
{name: "n=409600", max: 409600},
{name: "n=2394345", max: 2394345},
}
func TestInt(t *testing.T) {
for _, tt := range testIntCases {
t.Run(tt.name, func(t *testing.T) {
got, err := Int(tt.max)
if tt.errIs == nil || tt.errStr == "" {
assert.GreaterOrEqual(t, got, 0)
assert.LessOrEqual(t, got, tt.max)
}
if tt.errIs != nil {
assert.True(t, errors.Is(err, errInvalidMaxInt))
}
if tt.errStr != "" {
assert.EqualError(t, err, tt.errStr)
}
})
}
}
func BenchmarkInt(b *testing.B) {
for _, tt := range testIntCases {
b.Run(tt.name, func(b *testing.B) {
for n := 0; n < b.N; n++ {
_, _ = Int(tt.max)
}
})
}
}
func TestInt64(t *testing.T) {
for _, tt := range testIntCases {
t.Run(tt.name, func(t *testing.T) {
got, err := Int64(int64(tt.max))
if tt.errIs == nil || tt.errStr == "" {
assert.GreaterOrEqual(t, got, int64(0))
assert.LessOrEqual(t, got, int64(tt.max))
}
if tt.errIs != nil {
assert.True(t, errors.Is(err, errInvalidMaxInt))
}
if tt.errStr != "" {
assert.EqualError(t, err, tt.errStr)
}
})
}
}
func BenchmarkInt64(b *testing.B) {
for _, tt := range testIntCases {
b.Run(tt.name, func(b *testing.B) {
for n := 0; n < b.N; n++ {
_, _ = Int64(int64(tt.max))
}
})
}
}

11
rands.go Normal file
View File

@@ -0,0 +1,11 @@
// Package rands provides a suite of functions that use crypto/rand to generate
// cryptographically secure random strings in various formats, as well as ints
// and bytes.
//
// All functions which produce strings from a alphabet of characters uses
// rand.Int() to ensure a uniform distribution of all possible values.
package rands
import "errors"
var errBase = errors.New("rands")

18
rands_test.go Normal file
View File

@@ -0,0 +1,18 @@
package rands
var testCases = []struct {
name string
n int
}{
{name: "n=0", n: 0},
{name: "n=1", n: 1},
{name: "n=2", n: 2},
{name: "n=7", n: 7},
{name: "n=8", n: 8},
{name: "n=16", n: 16},
{name: "n=32", n: 32},
{name: "n=128", n: 128},
{name: "n=1024", n: 1024},
{name: "n=409600", n: 409600},
{name: "n=2394345", n: 2394345},
}

246
strings.go Normal file
View File

@@ -0,0 +1,246 @@
package rands
import (
"crypto/rand"
"encoding/base64"
"encoding/hex"
"fmt"
"math/big"
"unicode"
)
const (
upperChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
lowerChars = "abcdefghijklmnopqrstuvwxyz"
numericChars = "0123456789"
lowerNumericChars = lowerChars + numericChars
upperNumericChars = upperChars + numericChars
alphabeticChars = upperChars + lowerChars
alphanumericChars = alphabeticChars + numericChars
dnsLabelChars = lowerNumericChars + "-"
)
var (
errNonASCIIAlphabet = fmt.Errorf(
"%w: alphabet contains non-ASCII characters", errBase,
)
errDNSLabelLength = fmt.Errorf(
"%w: DNS labels must be between 1 and 63 characters in length", errBase,
)
)
// Base64 generates a random base64 encoded string of n number of bytes.
//
// Length of the returned string is about one third greater than the value of n,
// and it may contain characters A-Z, a-z, 0-9, "+", "/", and "=".
func Base64(n int) (string, error) {
b, err := Bytes(n)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(b), nil
}
// Base64URL generates a URL-safe un-padded random base64 encoded string of n
// number of bytes.
//
// Length of the returned string is about one third greater than the value of n,
// and it may contain characters A-Z, a-z, 0-9, "-", and "_".
func Base64URL(n int) (string, error) {
b, err := Bytes(n)
if err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(b), nil
}
// Hex generates a random hexadecimal encoded string of n number of bytes.
//
// Length of the returned string is twice the value of n, and it may contain
// characters 0-9 and a-f.
func Hex(n int) (string, error) {
b, err := Bytes(n)
if err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}
// Alphanumeric generates a random alphanumeric string of n length.
//
// The returned string may contain A-Z, a-z, and 0-9.
func Alphanumeric(n int) (string, error) {
return String(n, alphanumericChars)
}
// Alphabetic generates a random alphabetic string of n length.
//
// The returned string may contain A-Z, and a-z.
func Alphabetic(n int) (string, error) {
return String(n, alphabeticChars)
}
// Numeric generates a random numeric string of n length.
//
// The returned string may contain 0-9.
func Numeric(n int) (string, error) {
return String(n, numericChars)
}
// Upper generates a random uppercase alphabetic string of n length.
//
// The returned string may contain A-Z.
func Upper(n int) (string, error) {
return String(n, upperChars)
}
// UpperNumeric generates a random uppercase alphanumeric string of n length.
//
// The returned string may contain A-Z and 0-9.
func UpperNumeric(n int) (string, error) {
return String(n, upperNumericChars)
}
// Lower generates a random lowercase alphabetic string of n length.
//
// The returned string may contain a-z.
func Lower(n int) (string, error) {
return String(n, lowerChars)
}
// LowerNumeric generates a random lowercase alphanumeric string of n length.
//
// The returned string may contain A-Z and 0-9.
func LowerNumeric(n int) (string, error) {
return String(n, lowerNumericChars)
}
// String generates a random string of n length using the given ASCII alphabet.
//
// The specified alphabet determines what characters are used in the returned
// random string. The alphabet can only contain ASCII characters, use
// UnicodeString() if you need a alphabet with Unicode characters.
func String(n int, alphabet string) (string, error) {
if !isASCII(alphabet) {
return "", errNonASCIIAlphabet
}
l := big.NewInt(int64(len(alphabet)))
b := make([]byte, n)
for i := 0; i < n; i++ {
index, err := rand.Int(rand.Reader, l)
if err != nil {
return "", err
}
b[i] = alphabet[index.Int64()]
}
return string(b), nil
}
// UnicodeString generates a random string of n length using the given Unicode
// alphabet.
//
// The specified alphabet determines what characters are used in the returned
// random string. The length of the returned string will be n or greater
// depending on the byte-length of characters which were randomly selected from
// the alphabet.
func UnicodeString(n int, alphabet []rune) (string, error) {
l := big.NewInt(int64(len(alphabet)))
b := make([]rune, n)
for i := 0; i < n; i++ {
index, err := rand.Int(rand.Reader, l)
if err != nil {
return "", err
}
b[i] = alphabet[index.Int64()]
}
return string(b), nil
}
// DNSLabel returns a random string of n length in a DNS label compliant format
// as defined in RFC 1035, section 2.3.1, Preferred name syntax:
// https://tools.ietf.org/html/rfc1035#section-2.3.1
//
// It also adheres to RFC 5891, section 4.2.3.1, Hyphen Restrictions:
// https://tools.ietf.org/html/rfc5891#section-4.2.3.1
//
// In summary, the generated random string will:
//
// - be between 1 and 63 characters in length, other n values returns a error
// - first character will be one of a-z
// - last character will be one of a-z or 0-9
// - in-between first and last characters consist of a-z, 0-9, or "-"
// - potentially contain two or more consecutive "-", except the 3rd and 4th
// characters, as that would violate RFC 5891.
func DNSLabel(n int) (string, error) {
switch {
case n < 1 || n > 63:
return "", errDNSLabelLength
case n == 1:
return String(1, lowerChars)
default:
// First character of a DNS label allows only a-z characters.
head, err := String(1, lowerChars)
if err != nil {
return "", err
}
// Last character of a DNS label allows only a-z and 0-9 characters.
tail, err := String(1, lowerNumericChars)
if err != nil {
return "", err
}
if n < 3 {
return head + tail, nil
}
// The middle of a DNS label allows only a-z, 0-9, and "-" characters.
bodyLen := n - 2
body := make([]byte, bodyLen)
var last byte
var l *big.Int
for i := 0; i < bodyLen; i++ {
// Prevent two consecutive hyphens characters in positions 3 and 4,
// in accordance RFC 5891, section 4.2.3.1, Hyphen Restrictions:
// https://tools.ietf.org/html/rfc5891#section-4.2.3.1
if i == 2 && last == byte(45) {
l = big.NewInt(int64(len(lowerNumericChars)))
} else {
l = big.NewInt(int64(len(dnsLabelChars)))
}
index, err := rand.Int(rand.Reader, l)
if err != nil {
return "", err
}
if i == 2 && last == byte(45) {
last = lowerNumericChars[index.Int64()]
} else {
last = dnsLabelChars[index.Int64()]
}
body[i] = last
}
return head + string(body) + tail, nil
}
}
func isASCII(s string) bool {
for _, c := range s {
if c > unicode.MaxASCII {
return false
}
}
return true
}

72
strings_example_test.go Normal file
View File

@@ -0,0 +1,72 @@
package rands_test
import (
"fmt"
"github.com/jimeh/rands"
)
func ExampleBase64() {
s, _ := rands.Base64(16)
fmt.Println(s) // => CYxqEdUB1Rzno3SyZu2g/g==
}
func ExampleBase64URL() {
s, _ := rands.Base64URL(16)
fmt.Println(s) // => zlqw9aFqcFggbk2asn3_aQ
}
func ExampleHex() {
s, _ := rands.Hex(16)
fmt.Println(s) // => 956e2ec9e7f19ddd58bb935826926531
}
func ExampleAlphanumeric() {
s, _ := rands.Alphanumeric(16)
fmt.Println(s) // => Fvk1PkrmG5crgOjT
}
func ExampleAlphabetic() {
s, _ := rands.Alphabetic(16)
fmt.Println(s) // => XEJIzcZufHkuUmRM
}
func ExampleUpper() {
s, _ := rands.Upper(16)
fmt.Println(s) // => UMAGAFPPNDRGLUPZ
}
func ExampleUpperNumeric() {
s, _ := rands.UpperNumeric(16)
fmt.Println(s) // => DF0CQS0TK9CPUO3E
}
func ExampleLower() {
s, _ := rands.Lower(16)
fmt.Println(s) // => ocsmggykzrxzfwgt
}
func ExampleLowerNumeric() {
s, _ := rands.LowerNumeric(16)
fmt.Println(s) // => rwlv7a1p7klqffs5
}
func ExampleNumeric() {
s, _ := rands.Numeric(16)
fmt.Println(s) // => 9403373143598295
}
func ExampleString() {
s, _ := rands.String(16, "abcdefABCDEF")
fmt.Println(s) // => adCDCaDEdeffeDeb
}
func ExampleUnicodeString() {
s, _ := rands.UnicodeString(16, []rune("九七二人入八力十下三千上口土夕大"))
fmt.Println(s) // => 下下口九力下土夕下土八上二夕大三
}
func ExampleDNSLabel() {
s, _ := rands.DNSLabel(16)
fmt.Println(s) // => z0ij9o8qkbs0ru-h
}

623
strings_test.go Normal file
View File

@@ -0,0 +1,623 @@
package rands
import (
"encoding/base64"
"errors"
"regexp"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestHex(t *testing.T) {
allowed := "0123456789abcdef"
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
got, _ := Hex(tt.n)
assert.Len(t, got, tt.n*2)
assertAllowedChars(t, allowed, got)
})
}
}
func BenchmarkHex(b *testing.B) {
for _, tt := range testCases {
b.Run(tt.name, func(b *testing.B) {
for n := 0; n < b.N; n++ {
_, _ = Hex(tt.n)
}
})
}
}
func TestBase64(t *testing.T) {
allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" +
"0123456789+/="
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
got, _ := Base64(tt.n)
b, err := base64.StdEncoding.DecodeString(got)
require.NoError(t, err)
assert.Len(t, b, tt.n)
assertAllowedChars(t, allowed, got)
})
}
}
func BenchmarkBase64(b *testing.B) {
for _, tt := range testCases {
b.Run(tt.name, func(b *testing.B) {
for n := 0; n < b.N; n++ {
_, _ = Base64(tt.n)
}
})
}
}
func TestBase64URL(t *testing.T) {
allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" +
"0123456789-_"
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
got, _ := Base64URL(tt.n)
b, err := base64.RawURLEncoding.DecodeString(got)
require.NoError(t, err)
assert.Len(t, b, tt.n)
assertAllowedChars(t, allowed, got)
})
}
}
func BenchmarkBase64URL(b *testing.B) {
for _, tt := range testCases {
b.Run(tt.name, func(b *testing.B) {
for n := 0; n < b.N; n++ {
_, _ = Base64URL(tt.n)
}
})
}
}
func TestAlphanumeric(t *testing.T) {
allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
got, _ := Alphanumeric(tt.n)
assert.Len(t, got, tt.n)
assertAllowedChars(t, allowed, got)
})
}
}
func BenchmarkAlphanumeric(b *testing.B) {
for _, tt := range testCases {
b.Run(tt.name, func(b *testing.B) {
for n := 0; n < b.N; n++ {
_, _ = Alphanumeric(tt.n)
}
})
}
}
func TestAlphabetic(t *testing.T) {
allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
got, _ := Alphabetic(tt.n)
assert.Len(t, got, tt.n)
assertAllowedChars(t, allowed, got)
})
}
}
func BenchmarkAlphabetic(b *testing.B) {
for _, tt := range testCases {
b.Run(tt.name, func(b *testing.B) {
for n := 0; n < b.N; n++ {
_, _ = Alphabetic(tt.n)
}
})
}
}
func TestNumeric(t *testing.T) {
allowed := "0123456789"
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
got, _ := Numeric(tt.n)
assert.Len(t, got, tt.n)
assertAllowedChars(t, allowed, got)
})
}
}
func BenchmarkNumeric(b *testing.B) {
for _, tt := range testCases {
b.Run(tt.name, func(b *testing.B) {
for n := 0; n < b.N; n++ {
_, _ = Numeric(tt.n)
}
})
}
}
func TestUpper(t *testing.T) {
allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
got, _ := Upper(tt.n)
assert.Len(t, got, tt.n)
assertAllowedChars(t, allowed, got)
})
}
}
func BenchmarkUpper(b *testing.B) {
for _, tt := range testCases {
b.Run(tt.name, func(b *testing.B) {
for n := 0; n < b.N; n++ {
_, _ = Upper(tt.n)
}
})
}
}
func TestUpperNumeric(t *testing.T) {
allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
got, _ := UpperNumeric(tt.n)
assert.Len(t, got, tt.n)
assertAllowedChars(t, allowed, got)
})
}
}
func BenchmarkUpperNumeric(b *testing.B) {
for _, tt := range testCases {
b.Run(tt.name, func(b *testing.B) {
for n := 0; n < b.N; n++ {
_, _ = UpperNumeric(tt.n)
}
})
}
}
func TestLower(t *testing.T) {
allowed := "abcdefghijklmnopqrstuvwxyz"
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
got, _ := Lower(tt.n)
assert.Len(t, got, tt.n)
assertAllowedChars(t, allowed, got)
})
}
}
func BenchmarkLower(b *testing.B) {
for _, tt := range testCases {
b.Run(tt.name, func(b *testing.B) {
for n := 0; n < b.N; n++ {
_, _ = Lower(tt.n)
}
})
}
}
func TestLowerNumeric(t *testing.T) {
allowed := "abcdefghijklmnopqrstuvwxyz0123456789"
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
got, _ := LowerNumeric(tt.n)
assert.Len(t, got, tt.n)
assertAllowedChars(t, allowed, got)
})
}
}
func BenchmarkLowerNumeric(b *testing.B) {
for _, tt := range testCases {
b.Run(tt.name, func(b *testing.B) {
for n := 0; n < b.N; n++ {
_, _ = LowerNumeric(tt.n)
}
})
}
}
var stringTestCases = []struct {
name string
n int
alphabet string
errIs error
errStr string
}{
{
name: "greek",
n: 32,
alphabet: "αβγδεζηθικλμνξοπρστυφχψωςΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΩ" +
"άίόύώέϊϋΐΰΆΈΌΏΎΊ",
errIs: errNonASCIIAlphabet,
errStr: "rands: alphabet contains non-ASCII characters",
},
{
name: "chinese",
n: 32,
alphabet: "的一是不了人我在有他这为之大来以个中上们到说国和地也子" +
"时道出而要于就下得可你年生",
errIs: errNonASCIIAlphabet,
errStr: "rands: alphabet contains non-ASCII characters",
},
{
name: "japanese",
n: 32,
alphabet: "一九七二人入八力十下三千上口土夕大女子小山川五天中六円" +
"手文日月木水火犬王正出本右四",
errIs: errNonASCIIAlphabet,
errStr: "rands: alphabet contains non-ASCII characters",
},
{
name: "n=0",
n: 0,
alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-",
},
{
name: "n=1",
n: 1,
alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-",
},
{
name: "n=2",
n: 2,
alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-",
},
{
name: "n=7",
n: 7,
alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-",
},
{
name: "n=8",
n: 8,
alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-",
},
{
name: "n=16",
n: 16,
alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-",
},
{
name: "n=32",
n: 32,
alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-",
},
{
name: "n=128",
n: 128,
alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-",
},
{
name: "n=1024",
n: 1024,
alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-",
},
{
name: "n=409600",
n: 409600,
alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-",
},
{
name: "n=2394345",
n: 2394345,
alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-",
},
{
name: "uppercase",
n: 16,
alphabet: "ABCDEFGHJKMNPRSTUVWXYZ",
},
{
name: "lowercase",
n: 16,
alphabet: "abcdefghjkmnprstuvwxyz",
},
}
func TestString(t *testing.T) {
for _, tt := range stringTestCases {
t.Run(tt.name, func(t *testing.T) {
got, err := String(tt.n, tt.alphabet)
if tt.errIs == nil || tt.errStr == "" {
assert.Len(t, []rune(got), tt.n)
assertAllowedChars(t, tt.alphabet, got)
}
if tt.errIs != nil {
assert.True(t, errors.Is(err, errNonASCIIAlphabet))
}
if tt.errStr != "" {
assert.EqualError(t, err, tt.errStr)
}
})
}
}
func BenchmarkString(b *testing.B) {
for _, tt := range stringTestCases {
b.Run(tt.name, func(b *testing.B) {
for n := 0; n < b.N; n++ {
_, _ = String(tt.n, tt.alphabet)
}
})
}
}
var unicodeStringTestCases = []struct {
name string
n int
alphabet string
}{
{
name: "n=0",
n: 0,
alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" +
"手文日月木水火犬王正出本右",
},
{
name: "n=1",
n: 1,
alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" +
"手文日月木水火犬王正出本右",
},
{
name: "n=2",
n: 2,
alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" +
"手文日月木水火犬王正出本右",
},
{
name: "n=7",
n: 7,
alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" +
"手文日月木水火犬王正出本右",
},
{
name: "n=8",
n: 8,
alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" +
"手文日月木水火犬王正出本右",
},
{
name: "n=16",
n: 16,
alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" +
"手文日月木水火犬王正出本右",
},
{
name: "n=32",
n: 32,
alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" +
"手文日月木水火犬王正出本右",
},
{
name: "n=128",
n: 128,
alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" +
"手文日月木水火犬王正出本右",
},
{
name: "n=1024",
n: 1024,
alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" +
"手文日月木水火犬王正出本右",
},
{
name: "n=409600",
n: 409600,
alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" +
"手文日月木水火犬王正出本右",
},
{
name: "n=2394345",
n: 2394345,
alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" +
"手文日月木水火犬王正出本右",
},
{
name: "latin",
n: 32,
alphabet: "ABCDEFGHJKMNPRSTUVWXYZabcdefghjkmnprstuvwxyz",
},
{
name: "greek",
n: 32,
alphabet: "αβγδεζηθικλμνξοπρστυφχψωςΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΩ" +
"άίόύώέϊϋΐΰΆΈΌΏΎΊ",
},
{
name: "chinese",
n: 32,
alphabet: "的一是不了人我在有他这为之大来以个中上们到说国和地也子" +
"时道出而要于就下得可你年生",
},
{
name: "japanese",
n: 32,
alphabet: "一九七二人入八力十下三千上口土夕大女子小山川五天中六円" +
"手文日月木水火犬王正出本右四",
},
}
func TestUnicodeString(t *testing.T) {
for _, tt := range unicodeStringTestCases {
t.Run(tt.name, func(t *testing.T) {
got, _ := UnicodeString(tt.n, []rune(tt.alphabet))
assert.Len(t, []rune(got), tt.n)
assertAllowedChars(t, tt.alphabet, got)
})
}
}
func BenchmarkUnicodeString(b *testing.B) {
for _, tt := range stringTestCases {
alphabet := []rune(tt.alphabet)
b.Run(tt.name, func(b *testing.B) {
for n := 0; n < b.N; n++ {
_, _ = UnicodeString(tt.n, alphabet)
}
})
}
}
func TestDNSLabel(t *testing.T) {
tests := []struct {
name string
n int
errIs error
errStr string
}{
{
name: "n=-128",
n: -128,
errIs: errDNSLabelLength,
errStr: "rands: DNS labels must be between 1 and 63 characters " +
"in length",
},
{
name: "n=0",
n: 0,
errIs: errDNSLabelLength,
errStr: "rands: DNS labels must be between 1 and 63 characters " +
"in length",
},
{name: "n=1", n: 1},
{name: "n=2", n: 2},
{name: "n=3", n: 3},
{name: "n=4", n: 4},
{name: "n=5", n: 5},
{name: "n=6", n: 6},
{name: "n=7", n: 7},
{name: "n=8", n: 8},
{name: "n=16", n: 16},
{name: "n=32", n: 32},
{name: "n=63", n: 63},
{
name: "n=64",
n: 64,
errIs: errDNSLabelLength,
errStr: "rands: DNS labels must be between 1 and 63 characters " +
"in length",
},
{
name: "n=128",
n: 128,
errIs: errDNSLabelLength,
errStr: "rands: DNS labels must be between 1 and 63 characters " +
"in length",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// generate lots of labels to increase the chances of catching any
// obscure bugs
for i := 0; i < 10000; i++ {
got, err := DNSLabel(tt.n)
if tt.errIs == nil || tt.errStr == "" {
require.Len(t, got, tt.n)
asserDNSLabel(t, got)
}
if tt.errIs != nil {
require.True(t, errors.Is(err, errDNSLabelLength))
}
if tt.errStr != "" {
require.EqualError(t, err, tt.errStr)
}
}
})
}
}
//
// Helpers
//
var (
dnsLabelHeadRx = regexp.MustCompile(`^[a-z]$`)
dnsLabelBodyRx = regexp.MustCompile(`^[a-z0-9-]+$`)
dnsLabelTailRx = regexp.MustCompile(`^[a-z0-9]$`)
)
func asserDNSLabel(t *testing.T, label string) {
require.LessOrEqualf(t, len(label), 63,
`DNS label "%s" is longer than 63 characters`, label,
)
require.GreaterOrEqualf(t, len(label), 1,
`DNS label "%s" is shorter than 1 character`, label,
)
if len(label) >= 1 {
require.Regexpf(t, dnsLabelHeadRx, string(label[0]),
`DNS label "%s" must start with a-z`, label,
)
}
if len(label) >= 2 {
require.Regexpf(t, dnsLabelTailRx, string(label[len(label)-1]),
`DNS label "%s" must end with a-z0-9`, label,
)
}
if len(label) >= 3 {
require.Regexpf(t, dnsLabelBodyRx, label[1:len(label)-1],
`DNS label "%s" body must only contain a-z0-9-`, label)
}
if len(label) >= 4 {
require.NotEqualf(t, "--", label[2:4],
`DNS label "%s" cannot contain "--" as 3rd and 4th char`, label,
)
}
}
func assertAllowedChars(t *testing.T, allowed string, s string) {
invalid := ""
for _, c := range s {
if !strings.Contains(allowed, string(c)) &&
!strings.Contains(invalid, string(c)) {
invalid += string(c)
}
}
assert.Truef(
t, len(invalid) == 0, "string contains invalid chars: %s", invalid,
)
}